mauth-client 4.2.0 → 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: 2e197a763a48eeb58d88be12eab10dc6e2c7c104287b0371a41120bd920402db
4
- data.tar.gz: 84ef6da779ededf897bfe7a44eb2581073c2404a1208ae3a233b2d84c1bc98eb
3
+ metadata.gz: fb397d4c368ae894af1012c2305891bdfe4c85269e75e54167d4a7482a034f5a
4
+ data.tar.gz: 8dc255a280b8304360b305be500b00bf1a8205293a7061af866a99f2eb69ca10
5
5
  SHA512:
6
- metadata.gz: f640994364eee25edcbbf611a611eb539efc96432de37daf78c0211f83e966154c5dcfb321fff6e25e067fdb53faae6af792665897a7ae4d295beada833282d2
7
- data.tar.gz: 6270a48e063208842a1055d39814b96fe2cb4b8ceea04592eb94c62b533f110fe5510ca9060f321d5f4a309b96aaf9c99489ff28e068e5ddd6c04e20c1ae9bab
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/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
- ## v4.2.0
2
- * 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.
3
6
 
4
7
  ## v4.1.1
5
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 }
data/lib/mauth/client.rb CHANGED
@@ -7,139 +7,98 @@ require 'mauth/core_ext'
7
7
  require 'mauth/autoload'
8
8
  require 'mauth/dice_bag/mauth_templates'
9
9
  require 'mauth/version'
10
- require 'faraday-http-cache'
11
- require 'oj'
10
+ require 'mauth/client/authenticator_base'
11
+ require 'mauth/client/local_authenticator'
12
+ require 'mauth/client/remote_authenticator'
13
+ require 'mauth/client/signer'
14
+ require 'mauth/errors'
12
15
 
13
16
  module MAuth
17
+ # does operations which require a private key and corresponding app uuid. this is primarily:
18
+ # - signing outgoing requests and responses
19
+ # - authenticating incoming requests and responses, which may require retrieving the appropriate
20
+ # public key from mAuth (which requires a request to mAuth which is signed using the private
21
+ # key)
22
+ #
23
+ # this nominally operates on request and response objects, but really the only requirements are
24
+ # that the object responds to the methods of MAuth::Signable and/or MAuth::Signed (as
25
+ # appropriate)
14
26
  class Client
15
- class << self
16
- # returns a configuration (to be passed to MAuth::Client.new) which is configured from information stored in
17
- # standard places. all of which is overridable by options in case some defaults do not apply.
18
- #
19
- # options (may be symbols or strings) - any or all may be omitted where your usage conforms to the defaults.
20
- # - root: the path relative to which this method looks for configuration yaml files. defaults to Rails.root
21
- # if ::Rails is defined, otherwise ENV['RAILS_ROOT'], ENV['RACK_ROOT'], ENV['APP_ROOT'], or '.'
22
- # - environment: the environment, pertaining to top-level keys of the configuration yaml files. by default,
23
- # tries Rails.environment, ENV['RAILS_ENV'], and ENV['RACK_ENV'], and falls back to 'development' if none
24
- # of these are set.
25
- # - mauth_config - MAuth configuration. defaults to load this from a yaml file (see mauth_config_yml option)
26
- # which is assumed to be keyed with the environment at the root. if this is specified, no yaml file is
27
- # loaded, and the given config is passed through with any other defaults applied. at the moment, the only
28
- # other default is to set the logger.
29
- # - mauth_config_yml - specifies where a mauth configuration yaml file can be found. by default checks
30
- # ENV['MAUTH_CONFIG_YML'] or a file 'config/mauth.yml' relative to the root.
31
- # - logger - by default checks ::Rails.logger
32
- def default_config(options = {})
33
- options = options.stringify_symbol_keys
27
+ MWS_TOKEN = 'MWS'.freeze
28
+ MWSV2_TOKEN = 'MWSV2'.freeze
29
+ AUTH_HEADER_DELIMITER = ';'.freeze
34
30
 
35
- # find the app_root (relative to which we look for yaml files). note that this
36
- # is different than MAuth::Client.root, the root of the mauth-client library.
37
- app_root = options['root'] || begin
38
- if Object.const_defined?('Rails') && ::Rails.respond_to?(:root) && ::Rails.root
39
- Rails.root
40
- else
41
- ENV['RAILS_ROOT'] || ENV['RACK_ROOT'] || ENV['APP_ROOT'] || '.'
42
- end
43
- end
31
+ include AuthenticatorBase
32
+ include Signer
44
33
 
45
- # find the environment (with which yaml files are keyed)
46
- env = options['environment'] || begin
47
- if Object.const_defined?('Rails') && ::Rails.respond_to?(:environment)
48
- Rails.environment
49
- else
50
- ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
51
- end
34
+ # returns a configuration (to be passed to MAuth::Client.new) which is configured from information stored in
35
+ # standard places. all of which is overridable by options in case some defaults do not apply.
36
+ #
37
+ # options (may be symbols or strings) - any or all may be omitted where your usage conforms to the defaults.
38
+ # - root: the path relative to which this method looks for configuration yaml files. defaults to Rails.root
39
+ # if ::Rails is defined, otherwise ENV['RAILS_ROOT'], ENV['RACK_ROOT'], ENV['APP_ROOT'], or '.'
40
+ # - environment: the environment, pertaining to top-level keys of the configuration yaml files. by default,
41
+ # tries Rails.environment, ENV['RAILS_ENV'], and ENV['RACK_ENV'], and falls back to 'development' if none
42
+ # of these are set.
43
+ # - mauth_config - MAuth configuration. defaults to load this from a yaml file (see mauth_config_yml option)
44
+ # which is assumed to be keyed with the environment at the root. if this is specified, no yaml file is
45
+ # loaded, and the given config is passed through with any other defaults applied. at the moment, the only
46
+ # other default is to set the logger.
47
+ # - mauth_config_yml - specifies where a mauth configuration yaml file can be found. by default checks
48
+ # ENV['MAUTH_CONFIG_YML'] or a file 'config/mauth.yml' relative to the root.
49
+ # - logger - by default checks ::Rails.logger
50
+ def self.default_config(options = {})
51
+ options = options.stringify_symbol_keys
52
+
53
+ # find the app_root (relative to which we look for yaml files). note that this
54
+ # is different than MAuth::Client.root, the root of the mauth-client library.
55
+ app_root = options['root'] || begin
56
+ if Object.const_defined?('Rails') && ::Rails.respond_to?(:root) && ::Rails.root
57
+ Rails.root
58
+ else
59
+ ENV['RAILS_ROOT'] || ENV['RACK_ROOT'] || ENV['APP_ROOT'] || '.'
52
60
  end
61
+ end
53
62
 
54
- # find mauth config, given on options, or in a file at
55
- # ENV['MAUTH_CONFIG_YML'] or config/mauth.yml in the app_root
56
- mauth_config = options['mauth_config'] || begin
57
- mauth_config_yml = options['mauth_config_yml']
58
- mauth_config_yml ||= ENV['MAUTH_CONFIG_YML']
59
- default_loc = 'config/mauth.yml'
60
- default_yml = File.join(app_root, default_loc)
61
- mauth_config_yml ||= default_yml if File.exist?(default_yml)
62
- if mauth_config_yml && File.exist?(mauth_config_yml)
63
- whole_config = ConfigFile.load(mauth_config_yml)
64
- errmessage = "#{mauth_config_yml} config has no key #{env} - it has keys #{whole_config.keys.inspect}"
65
- whole_config[env] || raise(MAuth::Client::ConfigurationError, errmessage)
66
- else
67
- raise MAuth::Client::ConfigurationError, "could not find mauth config yaml file. this file may be " \
68
- "placed in #{default_loc}, specified with the mauth_config_yml option, or specified with the " \
69
- "MAUTH_CONFIG_YML environment variable."
70
- end
63
+ # find the environment (with which yaml files are keyed)
64
+ env = options['environment'] || begin
65
+ if Object.const_defined?('Rails') && ::Rails.respond_to?(:environment)
66
+ Rails.environment
67
+ else
68
+ ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
69
+ end
70
+ end
71
+
72
+ # find mauth config, given on options, or in a file at
73
+ # ENV['MAUTH_CONFIG_YML'] or config/mauth.yml in the app_root
74
+ mauth_config = options['mauth_config'] || begin
75
+ mauth_config_yml = options['mauth_config_yml']
76
+ mauth_config_yml ||= ENV['MAUTH_CONFIG_YML']
77
+ default_loc = 'config/mauth.yml'
78
+ default_yml = File.join(app_root, default_loc)
79
+ mauth_config_yml ||= default_yml if File.exist?(default_yml)
80
+ if mauth_config_yml && File.exist?(mauth_config_yml)
81
+ whole_config = ConfigFile.load(mauth_config_yml)
82
+ errmessage = "#{mauth_config_yml} config has no key #{env} - it has keys #{whole_config.keys.inspect}"
83
+ whole_config[env] || raise(MAuth::Client::ConfigurationError, errmessage)
84
+ else
85
+ raise MAuth::Client::ConfigurationError, "could not find mauth config yaml file. this file may be " \
86
+ "placed in #{default_loc}, specified with the mauth_config_yml option, or specified with the " \
87
+ "MAUTH_CONFIG_YML environment variable."
71
88
  end
89
+ end
72
90
 
73
- unless mauth_config.key?('logger')
74
- # the logger. Rails.logger if it exists, otherwise, no logger
75
- mauth_config['logger'] = options['logger'] || begin
76
- if Object.const_defined?('Rails') && ::Rails.respond_to?(:logger)
77
- Rails.logger
78
- end
91
+ unless mauth_config.key?('logger')
92
+ # the logger. Rails.logger if it exists, otherwise, no logger
93
+ mauth_config['logger'] = options['logger'] || begin
94
+ if Object.const_defined?('Rails') && ::Rails.respond_to?(:logger)
95
+ Rails.logger
79
96
  end
80
97
  end
81
-
82
- mauth_config
83
98
  end
84
- end
85
- end
86
99
 
87
- class ConfigFile
88
- GITHUB_URL = 'https://github.com/mdsol/mauth-client-ruby'.freeze
89
- @config = {}
90
-
91
- def self.load(path)
92
- unless File.exist?(path)
93
- raise "File #{path} not found. Please visit #{GITHUB_URL} for details."
94
- end
95
-
96
- @config[path] ||= YAML.load_file(path)
97
- unless @config[path]
98
- raise "File #{path} does not contain proper YAML information. Visit #{GITHUB_URL} for details."
99
- end
100
- @config[path]
100
+ mauth_config
101
101
  end
102
- end
103
- end
104
-
105
- module MAuth
106
- # mAuth client was unable to verify the authenticity of a signed object (this does NOT mean the
107
- # object is inauthentic). typically due to a failure communicating with the mAuth service, in
108
- # which case the error may include the attribute mauth_service_response - a response from
109
- # the mauth service (if it was contactable at all), which may contain more information about
110
- # the error.
111
- class UnableToAuthenticateError < StandardError
112
- # the response from the MAuth service encountered when attempting to retrieve authentication
113
- attr_accessor :mauth_service_response
114
- end
115
-
116
- # used to indicate that an object was expected to be validly signed but its signature does not
117
- # match its contents, and so is inauthentic.
118
- class InauthenticError < StandardError
119
- end
120
-
121
- # Used when the incoming request does not contain any mAuth related information
122
- class MauthNotPresent < StandardError
123
- end
124
-
125
-
126
- # required information for signing was missing
127
- class UnableToSignError < StandardError
128
- end
129
-
130
- # does operations which require a private key and corresponding app uuid. this is primarily:
131
- # - signing outgoing requests and responses
132
- # - authenticating incoming requests and responses, which may require retrieving the appropriate
133
- # public key from mAuth (which requires a request to mAuth which is signed using the private
134
- # key)
135
- #
136
- # this nominally operates on request and response objects, but really the only requirements are
137
- # that the object responds to the methods of MAuth::Signable and/or MAuth::Signed (as
138
- # appropriate)
139
- class Client
140
- class ConfigurationError < StandardError; end
141
-
142
- MWS_TOKEN = 'MWS'.freeze
143
102
 
144
103
  # new client with the given App UUID and public key. config may include the following (all
145
104
  # config keys may be strings or symbols):
@@ -192,6 +151,8 @@ module MAuth
192
151
  request_config.merge!(symbolize_keys(given_config['faraday_options'])) if given_config['faraday_options']
193
152
  @config['faraday_options'] = { request: request_config } || {}
194
153
  @config['ssl_certs_path'] = given_config['ssl_certs_path'] if given_config['ssl_certs_path']
154
+ @config['v2_only_authenticate'] = given_config['v2_only_authenticate'].to_s.downcase == 'true'
155
+ @config['v2_only_sign_requests'] = given_config['v2_only_sign_requests'].to_s.downcase == 'true'
195
156
 
196
157
  # if 'authenticator' was given, don't override that - including if it was given as nil / false
197
158
  if given_config.key?('authenticator')
@@ -236,6 +197,14 @@ module MAuth
236
197
  @config['ssl_certs_path']
237
198
  end
238
199
 
200
+ def v2_only_sign_requests?
201
+ @config['v2_only_sign_requests']
202
+ end
203
+
204
+ def v2_only_authenticate?
205
+ @config['v2_only_authenticate']
206
+ end
207
+
239
208
  def assert_private_key(err)
240
209
  raise err unless private_key
241
210
  end
@@ -257,259 +226,23 @@ module MAuth
257
226
  end
258
227
  hash
259
228
  end
229
+ end
260
230
 
261
- # methods to sign requests and responses. part of MAuth::Client
262
- module Signer
263
- # takes an outgoing request or response object, and returns an object of the same class
264
- # whose headers are updated to include mauth's signature headers
265
- def signed(object, attributes = {})
266
- object.merge_headers(signed_headers(object, attributes))
267
- end
268
-
269
- # takes a signable object (outgoing request or response). returns a hash of headers to be
270
- # applied tothe object which comprise its signature.
271
- def signed_headers(object, attributes = {})
272
- attributes = { time: Time.now.to_i.to_s, app_uuid: client_app_uuid }.merge(attributes)
273
- signature = self.signature(object, attributes)
274
- { 'X-MWS-Authentication' => "#{MWS_TOKEN} #{client_app_uuid}:#{signature}", 'X-MWS-Time' => attributes[:time] }
275
- end
276
-
277
- # takes a signable object (outgoing request or response). returns a mauth signature string
278
- # for that object.
279
- def signature(object, attributes = {})
280
- assert_private_key(UnableToSignError.new("mAuth client cannot sign without a private key!"))
281
- attributes = { time: Time.now.to_i.to_s, app_uuid: client_app_uuid }.merge(attributes)
282
- signature = Base64.encode64(private_key.private_encrypt(object.string_to_sign(attributes))).delete("\n")
283
- end
284
- end
285
- include Signer
286
-
287
- # methods common to RemoteRequestAuthenticator and LocalAuthenticator
288
- module Authenticator
289
- ALLOWED_DRIFT_SECONDS = 300
290
-
291
- # takes an incoming request or response object, and returns whether
292
- # the object is authentic according to its signature.
293
- def authentic?(object)
294
- log_authentication_request(object)
295
- begin
296
- authenticate!(object)
297
- true
298
- rescue InauthenticError, MauthNotPresent
299
- false
300
- end
301
- end
302
-
303
- # raises InauthenticError unless the given object is authentic
304
- def authenticate!(object)
305
- authentication_present!(object)
306
- time_valid!(object)
307
- token_valid!(object)
308
- signature_valid!(object)
309
- rescue MauthNotPresent => e
310
- logger.warn "mAuth signature not present on #{object.class}. Exception: #{e.message}"
311
- raise
312
- rescue InauthenticError => e
313
- logger.error "mAuth signature authentication failed for #{object.class}. Exception: #{e.message}"
314
- raise
315
- rescue UnableToAuthenticateError => e
316
- logger.error "Unable to authenticate with MAuth for #{object.class}. Exception: #{e.message}"
317
- raise
318
- end
319
-
320
- private
321
-
322
- # Note: This log is likely consumed downstream and the contents SHOULD NOT be changed without a thorough review of downstream consumers.
323
- def log_authentication_request(object)
324
- object_app_uuid = object.signature_app_uuid || '[none provided]'
325
- logger.info "Mauth-client attempting to authenticate request from app with mauth app uuid #{object_app_uuid} to app with mauth app uuid #{client_app_uuid}."
326
- end
327
-
328
- def authentication_present!(object)
329
- if object.x_mws_authentication.nil? || object.x_mws_authentication !~ /\S/
330
- raise MauthNotPresent, "Authentication Failed. No mAuth signature present; X-MWS-Authentication header is blank."
331
- end
332
- end
333
-
334
- def time_valid!(object, now = Time.now)
335
- if object.x_mws_time.nil?
336
- raise InauthenticError, "Time verification failed. No x-mws-time present."
337
- elsif !(-ALLOWED_DRIFT_SECONDS..ALLOWED_DRIFT_SECONDS).cover?(now.to_i - object.x_mws_time.to_i)
338
- raise InauthenticError, "Time verification failed. #{object.x_mws_time} not within #{ALLOWED_DRIFT_SECONDS} of #{now}"
339
- end
340
- end
341
-
342
- def token_valid!(object)
343
- unless object.signature_token == MWS_TOKEN
344
- raise InauthenticError, "Token verification failed. Expected #{MWS_TOKEN.inspect}; token was #{object.signature_token}"
345
- end
346
- end
347
- end
348
- include Authenticator
349
-
350
- # methods to verify the authenticity of signed requests and responses locally, retrieving
351
- # public keys from the mAuth service as needed
352
- module LocalAuthenticator
353
- private
354
-
355
- def signature_valid!(object)
356
- # We are in an unfortunate situation in which Euresource is percent-encoding parts of paths, but not
357
- # all of them. In particular, Euresource is percent-encoding all special characters save for '/'.
358
- # Also, unfortunately, Nginx unencodes URIs before sending them off to served applications, though
359
- # other web servers (particularly those we typically use for local testing) do not. The various forms
360
- # of the expected string to sign are meant to cover the main cases.
361
- # TODO: Revisit and simplify this unfortunate situation.
362
-
363
- original_request_uri = object.attributes_for_signing[:request_url]
364
-
365
- # craft an expected string-to-sign without doing any percent-encoding
366
- expected_no_reencoding = object.string_to_sign(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
367
-
368
- # do a simple percent reencoding variant of the path
369
- object.attributes_for_signing[:request_url] = CGI.escape(original_request_uri.to_s)
370
- expected_for_percent_reencoding = object.string_to_sign(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
371
-
372
- # do a moderately complex Euresource-style reencoding of the path
373
- object.attributes_for_signing[:request_url] = euresource_escape(original_request_uri.to_s)
374
- expected_euresource_style_reencoding = object.string_to_sign(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
375
-
376
- # reset the object original request_uri, just in case we need it again
377
- object.attributes_for_signing[:request_url] = original_request_uri
378
-
379
- pubkey = OpenSSL::PKey::RSA.new(retrieve_public_key(object.signature_app_uuid))
380
- begin
381
- actual = pubkey.public_decrypt(Base64.decode64(object.signature))
382
- rescue OpenSSL::PKey::PKeyError
383
- raise InauthenticError, "Public key decryption of signature failed!\n#{$!.class}: #{$!.message}"
384
- end
385
- # TODO: time-invariant comparison instead of #== ?
386
- unless expected_no_reencoding == actual || expected_euresource_style_reencoding == actual || expected_for_percent_reencoding == actual
387
- raise InauthenticError, "Signature verification failed for #{object.class}"
388
- end
389
- end
390
-
391
- # Note: RFC 3986 (https://www.ietf.org/rfc/rfc3986.txt) reserves the forward slash "/"
392
- # and number sign "#" as component delimiters. Since these are valid URI components,
393
- # they are decoded back into characters here to avoid signature invalidation
394
- def euresource_escape(str)
395
- CGI.escape(str).gsub(/%2F|%23/, "%2F" => "/", "%23" => "#")
396
- end
397
-
398
- def retrieve_public_key(app_uuid)
399
- retrieve_security_token(app_uuid)['security_token']['public_key_str']
400
- end
401
-
402
- def retrieve_security_token(app_uuid)
403
- security_token_cacher.get(app_uuid)
404
- end
405
-
406
- def security_token_cacher
407
- @security_token_cacher ||= SecurityTokenCacher.new(self)
408
- end
409
- class SecurityTokenCacher
410
-
411
- def initialize(mauth_client)
412
- @mauth_client = mauth_client
413
- # TODO: should this be UnableToSignError?
414
- @mauth_client.assert_private_key(UnableToAuthenticateError.new("Cannot fetch public keys from mAuth service without a private key!"))
415
- @cache = {}
416
- require 'thread'
417
- @cache_write_lock = Mutex.new
418
- end
419
-
420
- def get(app_uuid)
421
- if !@cache[app_uuid]
422
- # url-encode the app_uuid to prevent trickery like escaping upward with ../../ in a malicious
423
- # app_uuid - probably not exploitable, but this is the right way to do it anyway.
424
- # use UNRESERVED instead of UNSAFE (the default) as UNSAFE doesn't include /
425
- url_encoded_app_uuid = URI.escape(app_uuid, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
426
- begin
427
- response = signed_mauth_connection.get("/mauth/#{@mauth_client.mauth_api_version}/security_tokens/#{url_encoded_app_uuid}.json")
428
- rescue ::Faraday::Error::ConnectionFailed, ::Faraday::Error::TimeoutError
429
- raise UnableToAuthenticateError, "mAuth service did not respond; received #{$!.class}: #{$!.message}"
430
- end
431
- if response.status == 200
432
- @cache_write_lock.synchronize do
433
- @cache[app_uuid] = security_token_from(response.body)
434
- end
435
- elsif response.status == 404
436
- # signing with a key mAuth doesn't know about is considered inauthentic
437
- raise InauthenticError, "mAuth service responded with 404 looking up public key for #{app_uuid}"
438
- else
439
- @mauth_client.send(:mauth_service_response_error, response)
440
- end
441
- end
442
- @cache[app_uuid]
443
- end
444
-
445
- private
446
-
447
- def security_token_from(response_body)
448
- JSON.parse response_body
449
- rescue JSON::ParserError => e
450
- msg = "mAuth service responded with unparseable json: #{response_body}\n#{e.class}: #{e.message}"
451
- @mauth_client.logger.error("Unable to authenticate with MAuth. Exception #{msg}")
452
- raise UnableToAuthenticateError, msg
453
- end
231
+ module ConfigFile
232
+ GITHUB_URL = 'https://github.com/mdsol/mauth-client-ruby'.freeze
233
+ @config = {}
454
234
 
455
- def signed_mauth_connection
456
- require 'faraday'
457
- require 'mauth/faraday'
458
- @mauth_client.faraday_options[:ssl] = { ca_path: @mauth_client.ssl_certs_path } if @mauth_client.ssl_certs_path
459
- @signed_mauth_connection ||= ::Faraday.new(@mauth_client.mauth_baseurl, @mauth_client.faraday_options) do |builder|
460
- builder.use MAuth::Faraday::MAuthClientUserAgent
461
- builder.use MAuth::Faraday::RequestSigner, 'mauth_client' => @mauth_client
462
- builder.use :http_cache, serializer: Oj, logger: MAuth::Client.new.logger, shared_cache: false
463
- builder.adapter ::Faraday.default_adapter
464
- end
465
- end
235
+ def self.load(path)
236
+ unless File.exist?(path)
237
+ raise "File #{path} not found. Please visit #{GITHUB_URL} for details."
466
238
  end
467
- end
468
239
 
469
- # methods for remotely authenticating a request by sending it to the mauth service
470
- module RemoteRequestAuthenticator
471
- private
472
-
473
- # takes an incoming request object (no support for responses currently), and errors if the
474
- # object is not authentic according to its signature
475
- def signature_valid!(object)
476
- raise ArgumentError, "Remote Authenticator can only authenticate requests; received #{object.inspect}" unless object.is_a?(MAuth::Request)
477
- authentication_ticket = {
478
- 'verb' => object.attributes_for_signing[:verb],
479
- 'app_uuid' => object.signature_app_uuid,
480
- 'client_signature' => object.signature,
481
- 'request_url' => object.attributes_for_signing[:request_url],
482
- 'request_time' => object.x_mws_time,
483
- 'b64encoded_body' => Base64.encode64(object.attributes_for_signing[:body] || '')
484
- }
485
- begin
486
- response = mauth_connection.post("/mauth/#{mauth_api_version}/authentication_tickets.json", "authentication_ticket" => authentication_ticket)
487
- rescue ::Faraday::Error::ConnectionFailed, ::Faraday::Error::TimeoutError
488
- raise UnableToAuthenticateError, "mAuth service did not respond; received #{$!.class}: #{$!.message}"
489
- end
490
- if (200..299).cover?(response.status)
491
- nil
492
- elsif response.status == 412 || response.status == 404
493
- # the mAuth service responds with 412 when the given request is not authentically signed.
494
- # older versions of the mAuth service respond with 404 when the given app_uuid
495
- # does not exist, which is also considered to not be authentically signed. newer
496
- # versions of the service respond 412 in all cases, so the 404 check may be removed
497
- # when the old version of the mAuth service is out of service.
498
- raise InauthenticError, "The mAuth service responded with #{response.status}: #{response.body}"
499
- else
500
- mauth_service_response_error(response)
501
- end
240
+ @config[path] ||= YAML.load_file(path)
241
+ unless @config[path]
242
+ raise "File #{path} does not contain proper YAML information. Visit #{GITHUB_URL} for details."
502
243
  end
503
244
 
504
- def mauth_connection
505
- require 'faraday'
506
- require 'faraday_middleware'
507
- @mauth_connection ||= ::Faraday.new(mauth_baseurl, faraday_options) do |builder|
508
- builder.use MAuth::Faraday::MAuthClientUserAgent
509
- builder.use FaradayMiddleware::EncodeJson
510
- builder.adapter ::Faraday.default_adapter
511
- end
512
- end
245
+ @config[path]
513
246
  end
514
247
  end
515
248
  end