mauth-client 4.2.1 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a4124d677e7aee9626ea7e796bd5f92c695146cdadc125c2f05f3138d471758
4
- data.tar.gz: 4eda569778ce91ced2cedfd14a231281e35ec068b86d2e551d52541d05b3f795
3
+ metadata.gz: fb397d4c368ae894af1012c2305891bdfe4c85269e75e54167d4a7482a034f5a
4
+ data.tar.gz: 8dc255a280b8304360b305be500b00bf1a8205293a7061af866a99f2eb69ca10
5
5
  SHA512:
6
- metadata.gz: 6118dc54acd81a9dc16d1364fab960dee75c5a5e055fc79bb2517d6eb48e3dbf033078b913b231f430fc9ae953be5f56ab727664bc147b0e90f0f71203ec9f14
7
- data.tar.gz: fa8e3328ef7e779322e8ad0c7f4a3520f53370838a10e4cf1d0b8710ee0031fc37f4d5d31f080d39d7134c0dfa2a263a131c1323269e988a79978bcf21efd775
6
+ metadata.gz: 8e519a163cba44ca112f31d91a2da679601cebeac172eac15704f0ff93cc3ea5a3eba6e654d15cce3cbd2c587cc5bc3e6ae88a1042b84bdaa77f3e71fda01a96
7
+ data.tar.gz: 18e5b9888c5c56ffa2f78197cdca887958e220f6f63e71807393d043508b5cd013ded15031d29230262cdfeb0de86f1f71f18cffd62a64c5719da030a59915d8
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /log
8
8
 
9
9
  /Gemfile.lock
10
+ .byebug_history
data/.travis.yml CHANGED
@@ -13,23 +13,19 @@ before_install:
13
13
 
14
14
  install:
15
15
  - bundle install --jobs=3 --retry=3
16
- - >-
17
- curl -H 'Cache-Control: no-cache'
18
- https://raw.githubusercontent.com/mdsol/fossa_ci_scripts/main/travis_ci/fossa_install.sh |
19
- bash -s -- -b $TRAVIS_BUILD_DIR
16
+ - |-
17
+ curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/mdsol/fossa_ci_scripts/master/travis_ci/fossa_install.sh | bash -s -- -b $TRAVIS_BUILD_DIR
20
18
 
21
19
  script:
22
20
  - bundle exec rspec
23
- - >-
24
- curl -H 'Cache-Control: no-cache'
25
- https://raw.githubusercontent.com/mdsol/fossa_ci_scripts/main/travis_ci/fossa_run.sh |
26
- bash -s -- -b $TRAVIS_BUILD_DIR
21
+ - |-
22
+ curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/mdsol/fossa_ci_scripts/master/travis_ci/fossa_run.sh | bash -s -- -b $TRAVIS_BUILD_DIR
27
23
 
28
24
  deploy:
29
25
  provider: rubygems
30
26
  gem: mauth-client
31
27
  api_key:
32
- secure: QDp0P/lMGLYc4+A3M6VD9y551X6GrGwOSBE6xSG4lE6mPXoSISK5Yj18vNWQRQuQ4BsE6CdfZ/xsPjSRDda6b+yUQbgisjJ+Ry6jUVE1v9UKTZ0VHgHyXcsaJFC29tBKBeuGCj0AD5qhbTO1+ybeZSUfdSeVVoidD4W/bSnvzlT1Lht7IE8jbHbR57LsJKoEaDxKu33dg4CYV96xrlYGxHAS2UgEgi5Ve3ohzBWkX9RWF/wWoGCzIYhJBzXgCEEFw8iWkspjTePgv9yjD2HIMtF44aiSTHM5iqBBsYJ7A8+kUwoq7+srsashHZ1wZz1YulsCSkjwM9AXZ4E0f9AnERw/RQ5gG7bCuHZtSG9g/0SWBQeNfkAF3An6eTSS24KVfnarGdH2bk0G28k2oP26MWiDKz8nlQxNAY4rH+dITael18bgf45H4KccQqiooBEGnuYpUAuIPB+1l+BsIcRQnrU3LDtmtZn0KrCHHJ7EHOdogOG+/Pxof8ht1xF7V+HYhhzSRJs2JkvmZsp4q2T7W6b6kfi59Cz3LpqA1HHYcL5/OFZeLA/TlCNke0CRMxG8k3udDKj50jqFATXEa8lNyGLjmWh7tL9Bb/uy+CU47qUdx+V4K+kheAvNFtHfpxmyUGJSY0FH02H1VBPWm10DZ7kH+6jgCKyXuql+yWDw62s=
28
+ secure: J0aPDp4+Ev2L+ZDcgpF+hAG95S4IsD6pCiDRxDWnrk79P5hq1rXoD3S39ANyqtQEQqkoVjsgoSP5JLi420aL2lYj7mhvaEOty9fK+flwUhI4nw+Gztm7EKNDNX8WKvk4fl4Zc7noIeI0uyes867hDjRQfyYvUuma7aK5H9NWzNUV9Q+KrVAoneVDGnNydxwkuuIpOFdjbVQgNpxVhVBV7Q4OLsB1KtWB9lptMwhqnyqZKex7JZ+37sojaj3oVT5ijrnAm+bR1QO1hGIOwuBako2iz+MBZHPccM4BEFsZme/7olypxv0JfeCuhqDnH1VWIFh6IZRDeLnZuX3qOhkdx4HLwxB//5O5+iapK0wh1zbnLvXqkE1dalUHyaZzStKH9xchIWl5I77Ica232OJYrpj9hhroae0p3VARF0IoZceKaH8NnMpq+nBAW4REcWrqPpe9xkRLDTNibkpaAy08vGOF2kPZkWw4lfkVBM1+wjY2xDn6wJ7VgQ1BeosbeTXbmny2TUeI22beihn894tzpCPPHiTRvKu0lV3jBfeoOAXzE333PrGm3zF9MDhg+1/iBwXVhdoOwEwBPQ/3Hu37xJn0AfRneni4StYnIkZ1Ur9Vub03J/3C3Aw6it99rQSWvC+2PzHqQhsG22VprvxlozFe1jFzdqKgvDkbkn44ltI=
33
29
  on:
34
30
  tags: true
35
31
  repo: mdsol/mauth-client-ruby
data/CHANGELOG.md CHANGED
@@ -1,8 +1,8 @@
1
- ## v4.2.1
2
- * Fix SecurityTokenCacher to not cache tokens forever.
3
-
4
- ## v4.2.0
5
- * Drop legacy security token expiry in favor of honoring server cache headers via Faraday HTTP Cache Middleware.
1
+ ## v5.0.0
2
+ - Add support for MWSV2 protocol.
3
+ - Change request signing to sign with both V1 and V2 protocols by default.
4
+ - Update log message for authentication request to include protocol version used.
5
+ - Added `benchmark` rake task to benchmark request signing and authentication.
6
6
 
7
7
  ## v4.1.1
8
8
  - Use warning level instead of error level for logs about missing mauth header.
data/CONTRIBUTING.md CHANGED
@@ -18,3 +18,11 @@ Next, run the tests:
18
18
  ```
19
19
  bundle exec rspec
20
20
  ```
21
+
22
+ ## Running Benchmark
23
+
24
+ If you make changes which could affect performance, please run the benchmark before and after the change as a sanity check.
25
+
26
+ ```
27
+ bundle exec rake benchmark
28
+ ```
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # MAuth Client
1
+ # MAuth-Client
2
2
  [![Build Status](https://travis-ci.org/mdsol/mauth-client-ruby.svg?branch=master)](https://travis-ci.org/mdsol/mauth-client-ruby)
3
3
 
4
4
  This gem consists of MAuth::Client, a class to manage the information needed to both sign and authenticate requests
5
5
  and responses, and middlewares for Rack and Faraday which leverage the client's capabilities.
6
6
 
7
- MAuth Client exists in a variety of languages (.Net, Go, R etc.), see the [implementations list](doc/implementations.md) for more info.
7
+ MAuth-Client exists in a variety of languages (.Net, Go, R etc.), see the [implementations list](doc/implementations.md) for more info.
8
8
 
9
9
  ## Installation
10
10
 
@@ -47,12 +47,19 @@ Remote authentication therefore requires more time than local authentication.
47
47
  You will not be able to sign your responses without an `app_uuid` and a private key, so `MAuth::Rack::ResponseSigner` cannot be used.
48
48
 
49
49
  The `mauth_baseurl` and `mauth_api_version` are required in mauth.yml.
50
- These tell the MAuth Client where and how to communicate with the MAuth service.
50
+ These tell the MAuth-Client where and how to communicate with the MAuth service.
51
51
 
52
+ The `v2_only_sign_requests` and `v2_only_authenticate` flags were added to facilitate conversion from the MAuth V1 protocol to the MAuth
53
+ V2 protocol. By default both of these flags are false. See [Protocol Versions](#protocol-versions) below for more information about the different versions.
54
+
55
+ | | v2_only_sign_requests | v2_only_authenticate |
56
+ |-------|------------------------------------|--------------------------------------------------------------------------------------|
57
+ | true | requests are signed with only V2 | requests and responses are authenticated with only V2 |
58
+ | false | requests are signed with V1 and V2 | requests and responses are authenticated with the highest available protocol version |
52
59
 
53
60
  ## Rack Middleware Usage
54
61
 
55
- MAuth Client provides a middleware for request authentication and response verification in mauth/rack.
62
+ MAuth-Client provides a middleware for request authentication and response verification in mauth/rack.
56
63
 
57
64
  ```ruby
58
65
  require 'mauth/rack'
@@ -212,13 +219,13 @@ Create a `MAuth::Request` object from the information in your HTTP request, what
212
219
 
213
220
  ```ruby
214
221
  require 'mauth/request_and_response'
215
- request = MAuth::Request.new(verb: my_verb, request_url: my_request_url, body: my_body)
222
+ request = MAuth::Request.new(verb: my_verb, request_url: my_request_url, body: my_body, query_string: my_query_string)
216
223
  ```
217
224
  `mauth_client.signed_headers(request)` will then return mauth headers which you can apply to your request.
218
225
 
219
226
  ## Local Authentication
220
227
 
221
- When doing local authentication, the mauth client will periodically fetch and cache public keys from MAuth.
228
+ When doing local authentication, the MAuth-Client will periodically fetch and cache public keys from MAuth.
222
229
  Each public key will be cached locally for 60 seconds.
223
230
  Applications which connect frequently to the app will benefit most from this caching strategy.
224
231
  When fetching public keys from MAuth, the following rules apply:
@@ -233,3 +240,8 @@ When fetching public keys from MAuth, the following rules apply:
233
240
  During development classes are typically not cached in Rails applications.
234
241
  If this is the case, be aware that the MAuth-Client middleware object will be instantiated anew for each request;
235
242
  this will cause applications performing local authentication to fetch public keys before each request is authenticated.
243
+
244
+ ## Protocol Versions
245
+
246
+ The mauth V2 protocol was added as of v5.0.0. This protocol updates the string_to_sign to include query parameters, uses different authentication header names, and has a few other changes. See this document for more information: (DOC?). By default MAuth-Client will authenticate incoming requests with only the highest version of the protocol present, and sign their outgoing responses with only the version used to authenticate the request. By default MAuth-Client will sign outgoing requests with both the V1 and V2 protocols, and authenticate their incoming responses with only the highest version of the protocol present.
247
+ If the `v2_only_sign_requests` flag is true all outgoing requests will be signed with only the V2 protocol (outgoing responses will still be signed with whatever protocol used to authenticate the request). If the `v2_only_authenticate` flag is true then MAuth-Client will reject any incoming request or incoming response that does not use the V2 protocol.
data/Rakefile CHANGED
@@ -1,6 +1,113 @@
1
1
  require 'bundler/gem_tasks'
2
2
  require 'rspec/core/rake_task'
3
+ require 'mauth/request_and_response'
4
+ require 'mauth/client'
5
+ require 'securerandom'
6
+ require 'benchmark/ips'
7
+ require 'faraday'
8
+ require 'rspec/mocks/standalone'
3
9
 
4
10
  RSpec::Core::RakeTask.new(:spec)
5
11
 
6
12
  task default: :spec
13
+
14
+ class TestSignableRequest < MAuth::Request
15
+ include MAuth::Signed
16
+ attr_accessor :headers
17
+
18
+ def merge_headers(headers)
19
+ self.class.new(@attributes_for_signing).tap{|r| r.headers = (@headers || {}).merge(headers) }
20
+ end
21
+
22
+ def x_mws_time
23
+ headers['X-MWS-Time']
24
+ end
25
+
26
+ def x_mws_authentication
27
+ headers['X-MWS-Authentication']
28
+ end
29
+
30
+ def mcc_time
31
+ headers['MCC-Time']
32
+ end
33
+
34
+ def mcc_authentication
35
+ headers['MCC-Authentication']
36
+ end
37
+ end
38
+
39
+ desc 'Runs benchmarks for the library.'
40
+ task :benchmark do
41
+ mc = MAuth::Client.new(
42
+ private_key: OpenSSL::PKey::RSA.generate(2048),
43
+ app_uuid: SecureRandom.uuid,
44
+ v2_only_sign_requests: false
45
+ )
46
+ authenticating_mc = MAuth::Client.new(mauth_baseurl: 'http://whatever', mauth_api_version: 'v1')
47
+
48
+ stubs = Faraday::Adapter::Test::Stubs.new
49
+ test_faraday = ::Faraday.new do |builder|
50
+ builder.adapter(:test, stubs)
51
+ end
52
+ stubs.post('/mauth/v1/authentication_tickets.json') { [204, {}, []] }
53
+ allow(Faraday).to receive(:new).and_return(test_faraday)
54
+
55
+ short_body = 'Somewhere in La Mancha, in a place I do not care to remember'
56
+ average_body = short_body * 1_000
57
+ huge_body = average_body * 100
58
+
59
+ qs = 'don=quixote&quixote=don'
60
+
61
+ puts <<-MSG
62
+
63
+ A short request has a body of 60 chars.
64
+ An average request has a body of 60,000 chars.
65
+ A huge request has a body of 6,000,000 chars.
66
+ A qs request has a body of 60 chars and a query string with two k/v pairs.
67
+
68
+ MSG
69
+
70
+ short_request = TestSignableRequest.new(verb: 'PUT', request_url: '/', body: short_body)
71
+ qs_request = TestSignableRequest.new(verb: 'PUT', request_url: '/', body: short_body, query_string: qs)
72
+ average_request = TestSignableRequest.new(verb: 'PUT', request_url: '/', body: average_body)
73
+ huge_request = TestSignableRequest.new(verb: 'PUT', request_url: '/', body: huge_body)
74
+
75
+ v1_short_signed_request = mc.signed_v1(short_request)
76
+ v1_average_signed_request = mc.signed_v1(average_request)
77
+ v1_huge_signed_request = mc.signed_v1(huge_request)
78
+
79
+ v2_short_signed_request = mc.signed_v2(short_request)
80
+ v2_qs_signed_request = mc.signed_v1(qs_request)
81
+ v2_average_signed_request = mc.signed_v2(average_request)
82
+ v2_huge_signed_request = mc.signed_v1(huge_request)
83
+
84
+ Benchmark.ips do |bm|
85
+ bm.report('v1-sign-short') { mc.signed_v1(short_request) }
86
+ bm.report('v2-sign-short') { mc.signed_v2(short_request) }
87
+ bm.report('both-sign-short') { mc.signed(short_request) }
88
+ bm.report('v2-sign-qs') { mc.signed_v2(qs_request) }
89
+ bm.report('both-sign-qs') { mc.signed(qs_request) }
90
+ bm.report('v1-sign-average') { mc.signed_v1(average_request) }
91
+ bm.report('v2-sign-average') { mc.signed_v2(average_request) }
92
+ bm.report('both-sign-average') { mc.signed(average_request) }
93
+ bm.report('v1-sign-huge') { mc.signed_v1(huge_request) }
94
+ bm.report('v2-sign-huge') { mc.signed_v2(huge_request) }
95
+ bm.report('both-sign-huge') { mc.signed(huge_request) }
96
+ bm.compare!
97
+ end
98
+
99
+ puts "i/s means the number of signatures of a message per second.\n\n\n"
100
+
101
+ Benchmark.ips do |bm|
102
+ bm.report('v1-authenticate-short') { authenticating_mc.authentic?(v1_short_signed_request) }
103
+ bm.report('v2-authenticate-short') { authenticating_mc.authentic?(v2_short_signed_request) }
104
+ bm.report('v2-authenticate-qs') { authenticating_mc.authentic?(v2_qs_signed_request) }
105
+ bm.report('v1-authenticate-average') { authenticating_mc.authentic?(v1_average_signed_request) }
106
+ bm.report('v2-authenticate-average') { authenticating_mc.authentic?(v2_average_signed_request) }
107
+ bm.report('v1-authenticate-huge') { authenticating_mc.authentic?(v1_huge_signed_request) }
108
+ bm.report('v2-authenticate-huge') { authenticating_mc.authentic?(v2_huge_signed_request) }
109
+ bm.compare!
110
+ end
111
+
112
+ puts 'i/s means the number of authentication checks of signatures per second.'
113
+ end
data/exe/mauth-client CHANGED
@@ -11,7 +11,7 @@ require 'mauth/faraday'
11
11
  require 'yaml'
12
12
  require 'term/ansicolor'
13
13
 
14
- # OPTION PARSER
14
+ # OPTION PARSER
15
15
 
16
16
  require 'optparse'
17
17
 
@@ -53,10 +53,10 @@ end
53
53
  opt_parser.parse!
54
54
  abort(opt_parser.help) unless (2..3).include?(ARGV.size)
55
55
 
56
- # FIND MAUTH CONFIG
56
+ # FIND MAUTH CONFIG
57
57
 
58
58
  possible_mauth_config_files = [
59
- # whoops, I called this MAUTH_CONFIG_YML in one place and MAUTH_CONFIG_YAML in another. supporting both for now.
59
+ # whoops, I called this MAUTH_CONFIG_YML in one place and MAUTH_CONFIG_YAML in another. supporting both for now.
60
60
  ENV['MAUTH_CONFIG_YML'],
61
61
  ENV['MAUTH_CONFIG_YAML'],
62
62
  '~/.mauth_config.yml',
@@ -76,14 +76,14 @@ end
76
76
 
77
77
  mauth_config = MAuth::Client.default_config(:mauth_config_yml => File.expand_path(mauth_config_yml))
78
78
 
79
- # INSTANTIATE MAUTH CLIENT
79
+ # INSTANTIATE MAUTH CLIENT
80
80
 
81
81
  logger = Logger.new(STDERR)
82
82
  mauth_client = MAuth::Client.new(mauth_config.merge('logger' => logger))
83
83
 
84
- # OUTPUTTERS FOR FARADAY THAT SHOULD MOVE TO A LIB SOMEWHERE
84
+ # OUTPUTTERS FOR FARADAY THAT SHOULD MOVE TO A LIB SOMEWHERE
85
85
 
86
- # outputs the response body to the given output device (defaulting to STDOUT)
86
+ # outputs the response body to the given output device (defaulting to STDOUT)
87
87
  class FaradayOutputter < Faraday::Middleware
88
88
  def initialize(app, outdev=STDOUT)
89
89
  @app=app
@@ -97,12 +97,12 @@ class FaradayOutputter < Faraday::Middleware
97
97
  end
98
98
  end
99
99
 
100
- # this is to approximate `curl -v`s output. but it's all faked, whereas curl gives you
101
- # the real text written and read for request and response. whatever, close enough.
100
+ # this is to approximate `curl -v`s output. but it's all faked, whereas curl gives you
101
+ # the real text written and read for request and response. whatever, close enough.
102
102
  class FaradayCurlVOutputter < FaradayOutputter
103
103
 
104
- # defines a method with the given name, applying coloring defined by any additional arguments.
105
- # if $options[:color] is set, respects that; otherwise, applies color if the output device is a tty.
104
+ # defines a method with the given name, applying coloring defined by any additional arguments.
105
+ # if $options[:color] is set, respects that; otherwise, applies color if the output device is a tty.
106
106
  def self.color(name, *color_args)
107
107
  define_method(name) do |arg|
108
108
  if color?
@@ -159,8 +159,8 @@ class FaradayCurlVOutputter < FaradayOutputter
159
159
  $options[:color].nil? ? @outdev.tty? : $options[:color]
160
160
  end
161
161
 
162
- # a mapping for each registered CodeRay scanner to the Media Types which represent
163
- # that language. extremely incomplete!
162
+ # a mapping for each registered CodeRay scanner to the Media Types which represent
163
+ # that language. extremely incomplete!
164
164
  CodeRayForMediaTypes = {
165
165
  :c => [],
166
166
  :cpp => [],
@@ -184,7 +184,7 @@ class FaradayCurlVOutputter < FaradayOutputter
184
184
  }
185
185
 
186
186
  # takes a body and a content type; returns the body, with coloring (ansi colors for terminals)
187
- # possibly added, if it's a recognized content type and #color? is true
187
+ # possibly added, if it's a recognized content type and #color? is true
188
188
  def color_body_by_content_type(body, content_type)
189
189
  if body && color?
190
190
  # kinda hacky way to get the media_type. faraday should supply this ...
@@ -207,7 +207,7 @@ class FaradayCurlVOutputter < FaradayOutputter
207
207
  end
208
208
  end
209
209
 
210
- # CONFIGURE THE FARADAY CONNECTION
210
+ # CONFIGURE THE FARADAY CONNECTION
211
211
  faraday_options = {}
212
212
  if $options[:no_ssl_verify]
213
213
  faraday_options[:ssl] = {:verify => false}
@@ -233,8 +233,8 @@ if $options[:content_type]
233
233
  headers['Content-Type'] = $options[:content_type]
234
234
  else
235
235
  if body
236
- # I'd rather not have a default content-type, but if none is set then the HTTP adapter sets this to
237
- # application/x-www-form-urlencoded anyway. application/json is a better default for our purposes.
236
+ # I'd rather not have a default content-type, but if none is set then the HTTP adapter sets this to
237
+ # application/x-www-form-urlencoded anyway. application/json is a better default for our purposes.
238
238
  headers['Content-Type'] = 'application/json'
239
239
  end
240
240
  end
@@ -251,10 +251,10 @@ end
251
251
 
252
252
  begin
253
253
  response = connection.run_request(httpmethod.downcase.to_sym, url, body, headers)
254
- rescue MAuth::InauthenticError, MAuth::UnableToAuthenticateError => e
254
+ rescue MAuth::InauthenticError, MAuth::UnableToAuthenticateError, MAuth::MAuthNotPresent, MAuth::MissingV2Error => e
255
255
  if $options[:color].nil? ? STDERR.tty? : $options[:color]
256
- class_color = Term::ANSIColor.method(e.is_a?(MAuth::InauthenticError) ? :intense_red : :intense_yellow)
257
- message_color = Term::ANSIColor.method(e.is_a?(MAuth::InauthenticError) ? :red : :yellow)
256
+ class_color = Term::ANSIColor.method(e.is_a?(MAuth::UnableToAuthenticateError) ? :intense_yellow : :intense_red)
257
+ message_color = Term::ANSIColor.method(e.is_a?(MAuth::UnableToAuthenticateError) ? :yellow : :red)
258
258
  else
259
259
  class_color = proc{|s| s }
260
260
  message_color = proc{|s| s }
@@ -0,0 +1,118 @@
1
+ # methods common to RemoteRequestAuthenticator and LocalAuthenticator
2
+
3
+ module MAuth
4
+ class Client
5
+ module AuthenticatorBase
6
+ ALLOWED_DRIFT_SECONDS = 300
7
+
8
+ # takes an incoming request or response object, and returns whether
9
+ # the object is authentic according to its signature.
10
+ def authentic?(object)
11
+ log_authentication_request(object)
12
+ begin
13
+ authenticate!(object)
14
+ true
15
+ rescue InauthenticError, MAuthNotPresent, MissingV2Error
16
+ false
17
+ end
18
+ end
19
+
20
+ # raises InauthenticError unless the given object is authentic. Will only
21
+ # authenticate with v2 if the environment variable V2_ONLY_AUTHENTICATE
22
+ # is set. Otherwise will authenticate with only the highest protocol version present
23
+ def authenticate!(object)
24
+ if object.protocol_version == 2
25
+ authenticate_v2!(object)
26
+ elsif object.protocol_version == 1
27
+ if v2_only_authenticate?
28
+ # If v2 is required but not present and v1 is present we raise MissingV2Error
29
+ msg = 'This service requires mAuth v2 mcc-authentication header but only v1 x-mws-authentication is present'
30
+ logger.error(msg)
31
+ raise MissingV2Error, msg
32
+ end
33
+
34
+ authenticate_v1!(object)
35
+ else
36
+ sub_str = v2_only_authenticate? ? '' : 'X-MWS-Authentication header is blank, '
37
+ msg = "Authentication Failed. No mAuth signature present; #{sub_str}MCC-Authentication header is blank."
38
+ logger.warn("mAuth signature not present on #{object.class}. Exception: #{msg}")
39
+ raise MAuthNotPresent, msg
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ # Note: This log is likely consumed downstream and the contents SHOULD NOT
46
+ # be changed without a thorough review of downstream consumers.
47
+ def log_authentication_request(object)
48
+ object_app_uuid = object.signature_app_uuid || '[none provided]'
49
+ object_token = object.signature_token || '[none provided]'
50
+ logger.info(
51
+ "Mauth-client attempting to authenticate request from app with mauth" \
52
+ " app uuid #{object_app_uuid} to app with mauth app uuid #{client_app_uuid}" \
53
+ " using version #{object_token}."
54
+ )
55
+ end
56
+
57
+ def log_inauthentic(object, message)
58
+ logger.error("mAuth signature authentication failed for #{object.class}. Exception: #{message}")
59
+ end
60
+
61
+ def time_within_valid_range!(object, time_signed, now = Time.now)
62
+ return if (-ALLOWED_DRIFT_SECONDS..ALLOWED_DRIFT_SECONDS).cover?(now.to_i - time_signed)
63
+
64
+ msg = "Time verification failed. #{time_signed} not within #{ALLOWED_DRIFT_SECONDS} of #{now}"
65
+ log_inauthentic(object, msg)
66
+ raise InauthenticError, msg
67
+ end
68
+
69
+ # V1 helpers
70
+ def authenticate_v1!(object)
71
+ time_valid_v1!(object)
72
+ token_valid_v1!(object)
73
+ signature_valid_v1!(object)
74
+ end
75
+
76
+ def time_valid_v1!(object)
77
+ if object.x_mws_time.nil?
78
+ msg = 'Time verification failed. No x-mws-time present.'
79
+ log_inauthentic(object, msg)
80
+ raise InauthenticError, msg
81
+ end
82
+ time_within_valid_range!(object, object.x_mws_time.to_i)
83
+ end
84
+
85
+ def token_valid_v1!(object)
86
+ return if object.signature_token == MWS_TOKEN
87
+
88
+ msg = "Token verification failed. Expected #{MWS_TOKEN}; token was #{object.signature_token}"
89
+ log_inauthentic(object, msg)
90
+ raise InauthenticError, msg
91
+ end
92
+
93
+ # V2 helpers
94
+ def authenticate_v2!(object)
95
+ time_valid_v2!(object)
96
+ token_valid_v2!(object)
97
+ signature_valid_v2!(object)
98
+ end
99
+
100
+ def time_valid_v2!(object)
101
+ if object.mcc_time.nil?
102
+ msg = 'Time verification failed. No MCC-Time present.'
103
+ log_inauthentic(object, msg)
104
+ raise InauthenticError, msg
105
+ end
106
+ time_within_valid_range!(object, object.mcc_time.to_i)
107
+ end
108
+
109
+ def token_valid_v2!(object)
110
+ return if object.signature_token == MWSV2_TOKEN
111
+
112
+ msg = "Token verification failed. Expected #{MWSV2_TOKEN}; token was #{object.signature_token}"
113
+ log_inauthentic(object, msg)
114
+ raise InauthenticError, msg
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,137 @@
1
+ require 'mauth/client/security_token_cacher'
2
+ require 'mauth/client/signer'
3
+ require 'openssl'
4
+
5
+ # methods to verify the authenticity of signed requests and responses locally, retrieving
6
+ # public keys from the mAuth service as needed
7
+
8
+ module MAuth
9
+ class Client
10
+ module LocalAuthenticator
11
+ private
12
+
13
+ def signature_valid_v1!(object)
14
+ # We are in an unfortunate situation in which Euresource is percent-encoding parts of paths, but not
15
+ # all of them. In particular, Euresource is percent-encoding all special characters save for '/'.
16
+ # Also, unfortunately, Nginx unencodes URIs before sending them off to served applications, though
17
+ # other web servers (particularly those we typically use for local testing) do not. The various forms
18
+ # of the expected string to sign are meant to cover the main cases.
19
+ # TODO: Revisit and simplify this unfortunate situation.
20
+
21
+ original_request_uri = object.attributes_for_signing[:request_url]
22
+
23
+ # craft an expected string-to-sign without doing any percent-encoding
24
+ expected_no_reencoding = object.string_to_sign_v1(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
25
+
26
+ # do a simple percent reencoding variant of the path
27
+ object.attributes_for_signing[:request_url] = CGI.escape(original_request_uri.to_s)
28
+ expected_for_percent_reencoding = object.string_to_sign_v1(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
29
+
30
+ # do a moderately complex Euresource-style reencoding of the path
31
+ object.attributes_for_signing[:request_url] = euresource_escape(original_request_uri.to_s)
32
+ expected_euresource_style_reencoding = object.string_to_sign_v1(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
33
+
34
+ # reset the object original request_uri, just in case we need it again
35
+ object.attributes_for_signing[:request_url] = original_request_uri
36
+
37
+ begin
38
+ pubkey = OpenSSL::PKey::RSA.new(retrieve_public_key(object.signature_app_uuid))
39
+ actual = pubkey.public_decrypt(Base64.decode64(object.signature))
40
+ rescue OpenSSL::PKey::PKeyError => e
41
+ msg = "Public key decryption of signature failed! #{e.class}: #{e.message}"
42
+ log_inauthentic(object, msg)
43
+ raise InauthenticError, msg
44
+ end
45
+
46
+ unless verify_signature_v1!(actual, expected_no_reencoding) ||
47
+ verify_signature_v1!(actual, expected_euresource_style_reencoding) ||
48
+ verify_signature_v1!(actual, expected_for_percent_reencoding)
49
+ msg = "Signature verification failed for #{object.class}"
50
+ log_inauthentic(object, msg)
51
+ raise InauthenticError, msg
52
+ end
53
+ end
54
+
55
+ def verify_signature_v1!(actual, expected_str_to_sign)
56
+ actual == Digest::SHA512.hexdigest(expected_str_to_sign)
57
+ end
58
+
59
+ def signature_valid_v2!(object)
60
+ # We are in an unfortunate situation in which Euresource is percent-encoding parts of paths, but not
61
+ # all of them. In particular, Euresource is percent-encoding all special characters save for '/'.
62
+ # Also, unfortunately, Nginx unencodes URIs before sending them off to served applications, though
63
+ # other web servers (particularly those we typically use for local testing) do not. The various forms
64
+ # of the expected string to sign are meant to cover the main cases.
65
+ # TODO: Revisit and simplify this unfortunate situation.
66
+
67
+ original_request_uri = object.attributes_for_signing[:request_url]
68
+ original_query_string = object.attributes_for_signing[:query_string]
69
+
70
+ # craft an expected string-to-sign without doing any percent-encoding
71
+ expected_no_reencoding = object.string_to_sign_v2(
72
+ time: object.mcc_time,
73
+ app_uuid: object.signature_app_uuid
74
+ )
75
+
76
+ # do a simple percent reencoding variant of the path
77
+ expected_for_percent_reencoding = object.string_to_sign_v2(
78
+ time: object.mcc_time,
79
+ app_uuid: object.signature_app_uuid,
80
+ request_url: CGI.escape(original_request_uri.to_s),
81
+ query_string: CGI.escape(original_query_string.to_s)
82
+ )
83
+
84
+ # do a moderately complex Euresource-style reencoding of the path
85
+ expected_euresource_style_reencoding = object.string_to_sign_v2(
86
+ time: object.mcc_time,
87
+ app_uuid: object.signature_app_uuid,
88
+ request_url: euresource_escape(original_request_uri.to_s),
89
+ query_string: euresource_escape(original_query_string.to_s)
90
+ )
91
+
92
+ pubkey = OpenSSL::PKey::RSA.new(retrieve_public_key(object.signature_app_uuid))
93
+ actual = Base64.decode64(object.signature)
94
+
95
+ unless verify_signature_v2!(object, actual, pubkey, expected_no_reencoding) ||
96
+ verify_signature_v2!(object, actual, pubkey, expected_euresource_style_reencoding) ||
97
+ verify_signature_v2!(object, actual, pubkey, expected_for_percent_reencoding)
98
+ msg = "Signature inauthentic for #{object.class}"
99
+ log_inauthentic(object, msg)
100
+ raise InauthenticError, msg
101
+ end
102
+ end
103
+
104
+ def verify_signature_v2!(object, actual, pubkey, expected_str_to_sign)
105
+ pubkey.verify(
106
+ MAuth::Client::SIGNING_DIGEST,
107
+ actual,
108
+ expected_str_to_sign
109
+ )
110
+ rescue OpenSSL::PKey::PKeyError => e
111
+ msg = "RSA verification of signature failed! #{e.class}: #{e.message}"
112
+ log_inauthentic(object, msg)
113
+ raise InauthenticError, msg
114
+ end
115
+
116
+ # Note: RFC 3986 (https://www.ietf.org/rfc/rfc3986.txt) reserves the forward slash "/"
117
+ # and number sign "#" as component delimiters. Since these are valid URI components,
118
+ # they are decoded back into characters here to avoid signature invalidation
119
+ def euresource_escape(str)
120
+ CGI.escape(str).gsub(/%2F|%23/, '%2F' => '/', '%23' => '#')
121
+ end
122
+
123
+ def retrieve_public_key(app_uuid)
124
+ retrieve_security_token(app_uuid)['security_token']['public_key_str']
125
+ end
126
+
127
+ def retrieve_security_token(app_uuid)
128
+ security_token_cacher.get(app_uuid)
129
+ end
130
+
131
+ def security_token_cacher
132
+ @security_token_cacher ||= SecurityTokenCacher.new(self)
133
+ end
134
+
135
+ end
136
+ end
137
+ end