mauth-client 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,487 @@
1
+ require 'uri'
2
+ require 'openssl'
3
+ require 'base64'
4
+ require 'json'
5
+ require 'yaml'
6
+ require 'mauth/core_ext'
7
+ require 'mauth/autoload'
8
+ require 'mauth/dice_bag/mauth_templates'
9
+ require 'mauth/version'
10
+
11
+ module MAuth
12
+ class Client
13
+ class << self
14
+ # returns a configuration (to be passed to MAuth::Client.new) which is configured from information stored in
15
+ # standard places. all of which is overridable by options in case some defaults do not apply.
16
+ #
17
+ # options (may be symbols or strings) - any or all may be omitted where your usage conforms to the defaults.
18
+ # - root: the path relative to which this method looks for configuration yaml files. defaults to Rails.root
19
+ # if ::Rails is defined, otherwise ENV['RAILS_ROOT'], ENV['RACK_ROOT'], ENV['APP_ROOT'], or '.'
20
+ # - environment: the environment, pertaining to top-level keys of the configuration yaml files. by default,
21
+ # tries Rails.environment, ENV['RAILS_ENV'], and ENV['RACK_ENV'], and falls back to 'development' if none
22
+ # of these are set.
23
+ # - mauth_config - MAuth configuration. defaults to load this from a yaml file (see mauth_config_yml option)
24
+ # which is assumed to be keyed with the environment at the root. if this is specified, no yaml file is
25
+ # loaded, and the given config is passed through with any other defaults applied. at the moment, the only
26
+ # other default is to set the logger.
27
+ # - mauth_config_yml - specifies where a mauth configuration yaml file can be found. by default checks
28
+ # ENV['MAUTH_CONFIG_YML'] or a file 'config/mauth.yml' relative to the root.
29
+ # - logger - by default checks ::Rails.logger
30
+ def default_config(options = {})
31
+ options = options.stringify_symbol_keys
32
+
33
+ # find the app_root (relative to which we look for yaml files). note that this
34
+ # is different than MAuth::Client.root, the root of the mauth-client library.
35
+ app_root = options['root'] || begin
36
+ if Object.const_defined?('Rails') && ::Rails.respond_to?(:root) && ::Rails.root
37
+ Rails.root
38
+ else
39
+ ENV['RAILS_ROOT'] || ENV['RACK_ROOT'] || ENV['APP_ROOT'] || '.'
40
+ end
41
+ end
42
+
43
+ # find the environment (with which yaml files are keyed)
44
+ env = options['environment'] || begin
45
+ if Object.const_defined?('Rails') && ::Rails.respond_to?(:environment)
46
+ Rails.environment
47
+ else
48
+ ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
49
+ end
50
+ end
51
+
52
+ # find mauth config, given on options, or in a file at
53
+ # ENV['MAUTH_CONFIG_YML'] or config/mauth.yml in the app_root
54
+ mauth_config = options['mauth_config'] || begin
55
+ mauth_config_yml = options['mauth_config_yml']
56
+ mauth_config_yml ||= ENV['MAUTH_CONFIG_YML']
57
+ default_loc = 'config/mauth.yml'
58
+ default_yml = File.join(app_root, default_loc)
59
+ mauth_config_yml ||= default_yml if File.exist?(default_yml)
60
+ if mauth_config_yml && File.exist?(mauth_config_yml)
61
+ whole_config = YAML.load_file(mauth_config_yml)
62
+ errmessage = "#{mauth_config_yml} config has no key #{env} - it has keys #{whole_config.keys.inspect}"
63
+ whole_config[env] || raise(MAuth::Client::ConfigurationError, errmessage)
64
+ else
65
+ raise MAuth::Client::ConfigurationError, "could not find mauth config yaml file. this file may be " \
66
+ "placed in #{default_loc}, specified with the mauth_config_yml option, or specified with the " \
67
+ "MAUTH_CONFIG_YML environment variable."
68
+ end
69
+ end
70
+
71
+ unless mauth_config.key?('logger')
72
+ # the logger. Rails.logger if it exists, otherwise, no logger
73
+ mauth_config['logger'] = options['logger'] || begin
74
+ if Object.const_defined?('Rails') && ::Rails.respond_to?(:logger)
75
+ Rails.logger
76
+ end
77
+ end
78
+ end
79
+
80
+ mauth_config
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ module MAuth
87
+ # mAuth client was unable to verify the authenticity of a signed object (this does NOT mean the
88
+ # object is inauthentic). typically due to a failure communicating with the mAuth service, in
89
+ # which case the error may include the attribute mauth_service_response - a response from
90
+ # the mauth service (if it was contactable at all), which may contain more information about
91
+ # the error.
92
+ class UnableToAuthenticateError < StandardError
93
+ # the response from the MAuth service encountered when attempting to retrieve authentication
94
+ attr_accessor :mauth_service_response
95
+ end
96
+
97
+ # used to indicate that an object was expected to be validly signed but its signature does not
98
+ # match its contents, and so is inauthentic.
99
+ class InauthenticError < StandardError
100
+ end
101
+
102
+ # required information for signing was missing
103
+ class UnableToSignError < StandardError
104
+ end
105
+
106
+ # does operations which require a private key and corresponding app uuid. this is primarily:
107
+ # - signing outgoing requests and responses
108
+ # - authenticating incoming requests and responses, which may require retrieving the appropriate
109
+ # public key from mAuth (which requires a request to mAuth which is signed using the private
110
+ # key)
111
+ #
112
+ # this nominally operates on request and response objects, but really the only requirements are
113
+ # that the object responds to the methods of MAuth::Signable and/or MAuth::Signed (as
114
+ # appropriate)
115
+ class Client
116
+ class ConfigurationError < StandardError; end
117
+
118
+ MWS_TOKEN = 'MWS'.freeze
119
+
120
+ # new client with the given App UUID and public key. config may include the following (all
121
+ # config keys may be strings or symbols):
122
+ # - private_key - required for signing and for authenticating responses. may be omitted if
123
+ # only remote authentication of requests is being performed (with
124
+ # MAuth::Rack::RequestAuthenticator). may be given as a string or a OpenSSL::PKey::RSA
125
+ # instance.
126
+ # - app_uuid - required in the same circumstances where a private_key is required
127
+ # - mauth_baseurl - required. needed for local authentication to retrieve public keys; needed
128
+ # for remote authentication for hopefully obvious reasons.
129
+ # - mauth_api_version - required. only 'v1' exists / is supported as of this writing.
130
+ # - logger - a Logger to which any useful information will be written. if this is omitted and
131
+ # Rails.logger exists, that will be used.
132
+ # - authenticator - this pretty much never needs to be specified. LocalAuthenticator or
133
+ # RemoteRequestAuthenticator will be used as appropriate.
134
+ def initialize(config = {})
135
+ # stringify symbol keys
136
+ given_config = config.stringify_symbol_keys
137
+ # build a configuration which discards any irrelevant parts of the given config (small memory usage matters here)
138
+ @config = {}
139
+ if given_config['private_key_file'] && !given_config['private_key']
140
+ given_config['private_key'] = File.read(given_config['private_key_file'])
141
+ end
142
+ @config['private_key'] = case given_config['private_key']
143
+ when nil
144
+ nil
145
+ when String
146
+ OpenSSL::PKey::RSA.new(given_config['private_key'])
147
+ when OpenSSL::PKey::RSA
148
+ given_config['private_key']
149
+ else
150
+ raise MAuth::Client::ConfigurationError, "unrecognized value given for 'private_key' - this may be a " \
151
+ "String, a OpenSSL::PKey::RSA, or omitted; instead got: #{given_config['private_key'].inspect}"
152
+ end
153
+ @config['app_uuid'] = given_config['app_uuid']
154
+ @config['mauth_baseurl'] = given_config['mauth_baseurl']
155
+ @config['mauth_api_version'] = given_config['mauth_api_version']
156
+ @config['logger'] = given_config['logger'] || begin
157
+ if Object.const_defined?('Rails') && Rails.logger
158
+ Rails.logger
159
+ else
160
+ require 'logger'
161
+ is_win = RUBY_PLATFORM =~ /mswin|windows|mingw32|cygwin/i
162
+ null_device = is_win ? 'NUL' : '/dev/null'
163
+ ::Logger.new(File.open(null_device, File::WRONLY))
164
+ end
165
+ end
166
+
167
+ request_config = { timeout: 10, open_timeout: 10 }
168
+ request_config.merge!(symbolize_keys(given_config['faraday_options'])) if given_config['faraday_options']
169
+ @config['faraday_options'] = { request: request_config } || {}
170
+ @config['ssl_certs_path'] = given_config['ssl_certs_path'] if given_config['ssl_certs_path']
171
+
172
+ # if 'authenticator' was given, don't override that - including if it was given as nil / false
173
+ if given_config.key?('authenticator')
174
+ @config['authenticator'] = given_config['authenticator']
175
+ else
176
+ if client_app_uuid && private_key
177
+ # MAuth::Client can authenticate locally if it's provided a client_app_uuid and private_key
178
+ @config['authenticator'] = LocalAuthenticator
179
+ else
180
+ # otherwise, it will authenticate remotely (requests only)
181
+ @config['authenticator'] = RemoteRequestAuthenticator
182
+ end
183
+ end
184
+ extend @config['authenticator'] if @config['authenticator']
185
+ end
186
+
187
+ def logger
188
+ @config['logger']
189
+ end
190
+
191
+ def client_app_uuid
192
+ @config['app_uuid']
193
+ end
194
+
195
+ def mauth_baseurl
196
+ @config['mauth_baseurl'] || raise(MAuth::Client::ConfigurationError, "no configured mauth_baseurl!")
197
+ end
198
+
199
+ def mauth_api_version
200
+ @config['mauth_api_version'] || raise(MAuth::Client::ConfigurationError, "no configured mauth_api_version!")
201
+ end
202
+
203
+ def private_key
204
+ @config['private_key']
205
+ end
206
+
207
+ def faraday_options
208
+ @config['faraday_options']
209
+ end
210
+
211
+ def ssl_certs_path
212
+ @config['ssl_certs_path']
213
+ end
214
+
215
+ def assert_private_key(err)
216
+ raise err unless private_key
217
+ end
218
+
219
+ private
220
+
221
+ def mauth_service_response_error(response)
222
+ message = "mAuth service responded with #{response.status}: #{response.body}"
223
+ logger.error(message)
224
+ error = UnableToAuthenticateError.new(message)
225
+ error.mauth_service_response = response
226
+ raise error
227
+ end
228
+
229
+ # Changes all keys in the top level of the hash to symbols. Does not affect nested hashes inside this one.
230
+ def symbolize_keys(hash)
231
+ hash.keys.each do |key|
232
+ hash[(key.to_sym rescue key) || key] = hash.delete(key)
233
+ end
234
+ hash
235
+ end
236
+
237
+ # methods to sign requests and responses. part of MAuth::Client
238
+ module Signer
239
+ # takes an outgoing request or response object, and returns an object of the same class
240
+ # whose headers are updated to include mauth's signature headers
241
+ def signed(object, attributes = {})
242
+ object.merge_headers(signed_headers(object, attributes))
243
+ end
244
+
245
+ # takes a signable object (outgoing request or response). returns a hash of headers to be
246
+ # applied tothe object which comprise its signature.
247
+ def signed_headers(object, attributes = {})
248
+ attributes = { time: Time.now.to_i.to_s, app_uuid: client_app_uuid }.merge(attributes)
249
+ signature = self.signature(object, attributes)
250
+ { 'X-MWS-Authentication' => "#{MWS_TOKEN} #{client_app_uuid}:#{signature}", 'X-MWS-Time' => attributes[:time] }
251
+ end
252
+
253
+ # takes a signable object (outgoing request or response). returns a mauth signature string
254
+ # for that object.
255
+ def signature(object, attributes = {})
256
+ assert_private_key(UnableToSignError.new("mAuth client cannot sign without a private key!"))
257
+ attributes = { time: Time.now.to_i.to_s, app_uuid: client_app_uuid }.merge(attributes)
258
+ signature = Base64.encode64(private_key.private_encrypt(object.string_to_sign(attributes))).delete("\n")
259
+ end
260
+ end
261
+ include Signer
262
+
263
+ # methods common to RemoteRequestAuthenticator and LocalAuthenticator
264
+ module Authenticator
265
+ ALLOWED_DRIFT_SECONDS = 300
266
+
267
+ # takes an incoming request or response object, and returns whether
268
+ # the object is authentic according to its signature.
269
+ def authentic?(object)
270
+ log_authentication_request(object)
271
+ begin
272
+ authenticate!(object)
273
+ true
274
+ rescue InauthenticError
275
+ false
276
+ end
277
+ end
278
+
279
+ # raises InauthenticError unless the given object is authentic
280
+ def authenticate!(object)
281
+ authentication_present!(object)
282
+ time_valid!(object)
283
+ token_valid!(object)
284
+ signature_valid!(object)
285
+ rescue InauthenticError
286
+ logger.error "mAuth signature authentication failed for #{object.class}. encountered error:"
287
+ $!.message.split("\n").each { |l| logger.error "\t#{l}" }
288
+ raise
289
+ rescue UnableToAuthenticateError
290
+ logger.error "Unable to authenticate with MAuth. encountered error:"
291
+ $!.message.split("\n").each { |l| logger.error "\t#{l}" }
292
+ raise
293
+ end
294
+
295
+ private
296
+
297
+ # Note: This log is likely consumed downstream and the contents SHOULD NOT be changed without a thorough review of downstream consumers.
298
+ def log_authentication_request(object)
299
+ object_app_uuid = object.signature_app_uuid || '[none provided]'
300
+ 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}."
301
+ rescue # don't let a failed attempt to log disrupt the rest of the action
302
+ logger.error "Mauth-client failed to log information about its attempts to authenticate the current request because #{$!}"
303
+ end
304
+
305
+ def authentication_present!(object)
306
+ if object.x_mws_authentication.nil? || object.x_mws_authentication !~ /\S/
307
+ raise InauthenticError, "Authentication Failed. No mAuth signature present; X-MWS-Authentication header is blank."
308
+ end
309
+ end
310
+
311
+ def time_valid!(object, now = Time.now)
312
+ if object.x_mws_time.nil?
313
+ raise InauthenticError, "Time verification failed for #{object.class}. No x-mws-time present."
314
+ elsif !(-ALLOWED_DRIFT_SECONDS..ALLOWED_DRIFT_SECONDS).cover?(now.to_i - object.x_mws_time.to_i)
315
+ raise InauthenticError, "Time verification failed for #{object.class}. #{object.x_mws_time} not within #{ALLOWED_DRIFT_SECONDS} of #{now}"
316
+ end
317
+ end
318
+
319
+ def token_valid!(object)
320
+ unless object.signature_token == MWS_TOKEN
321
+ raise InauthenticError, "Token verification failed for #{object.class}. Expected #{MWS_TOKEN.inspect}; token was #{object.signature_token}"
322
+ end
323
+ end
324
+ end
325
+ include Authenticator
326
+
327
+ # methods to verify the authenticity of signed requests and responses locally, retrieving
328
+ # public keys from the mAuth service as needed
329
+ module LocalAuthenticator
330
+ private
331
+
332
+ def signature_valid!(object)
333
+ # We are in an unfortunate situation in which Euresource is percent-encoding parts of paths, but not
334
+ # all of them. In particular, Euresource is percent-encoding all special characters save for '/'.
335
+ # Also, unfortunately, Nginx unencodes URIs before sending them off to served applications, though
336
+ # other web servers (particularly those we typically use for local testing) do not. The various forms
337
+ # of the expected string to sign are meant to cover the main cases.
338
+ # TODO: Revisit and simplify this unfortunate situation.
339
+
340
+ original_request_uri = object.attributes_for_signing[:request_url]
341
+
342
+ # craft an expected string-to-sign without doing any percent-encoding
343
+ expected_no_reencoding = object.string_to_sign(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
344
+
345
+ # do a simple percent reencoding variant of the path
346
+ object.attributes_for_signing[:request_url] = CGI.escape(original_request_uri.to_s)
347
+ expected_for_percent_reencoding = object.string_to_sign(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
348
+
349
+ # do a moderately complex Euresource-style reencoding of the path
350
+ object.attributes_for_signing[:request_url] = CGI.escape(original_request_uri.to_s)
351
+ object.attributes_for_signing[:request_url].gsub!('%2F', '/') # ...and then 'simply' decode the %2F's back into /'s, just like Euresource kind of does!
352
+ expected_euresource_style_reencoding = object.string_to_sign(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
353
+
354
+ # reset the object original request_uri, just in case we need it again
355
+ object.attributes_for_signing[:request_url] = original_request_uri
356
+
357
+ pubkey = OpenSSL::PKey::RSA.new(retrieve_public_key(object.signature_app_uuid))
358
+ begin
359
+ actual = pubkey.public_decrypt(Base64.decode64(object.signature))
360
+ rescue OpenSSL::PKey::PKeyError
361
+ raise InauthenticError, "Public key decryption of signature failed!\n#{$!.class}: #{$!.message}"
362
+ end
363
+ # TODO: time-invariant comparison instead of #== ?
364
+ unless expected_no_reencoding == actual || expected_euresource_style_reencoding == actual || expected_for_percent_reencoding == actual
365
+ raise InauthenticError, "Signature verification failed for #{object.class}"
366
+ end
367
+ end
368
+
369
+ def retrieve_public_key(app_uuid)
370
+ retrieve_security_token(app_uuid)['security_token']['public_key_str']
371
+ end
372
+
373
+ def retrieve_security_token(app_uuid)
374
+ security_token_cacher.get(app_uuid)
375
+ end
376
+
377
+ def security_token_cacher
378
+ @security_token_cacher ||= SecurityTokenCacher.new(self)
379
+ end
380
+ class SecurityTokenCacher
381
+ class ExpirableSecurityToken < Struct.new(:security_token, :create_time)
382
+ CACHE_LIFE = 60
383
+ def expired?
384
+ create_time + CACHE_LIFE < Time.now
385
+ end
386
+ end
387
+ def initialize(mauth_client)
388
+ @mauth_client = mauth_client
389
+ # TODO: should this be UnableToSignError?
390
+ @mauth_client.assert_private_key(UnableToAuthenticateError.new("Cannot fetch public keys from mAuth service without a private key!"))
391
+ @cache = {}
392
+ require 'thread'
393
+ @cache_write_lock = Mutex.new
394
+ end
395
+
396
+ def get(app_uuid)
397
+ if !@cache[app_uuid] || @cache[app_uuid].expired?
398
+ # url-encode the app_uuid to prevent trickery like escaping upward with ../../ in a malicious
399
+ # app_uuid - probably not exploitable, but this is the right way to do it anyway.
400
+ # use UNRESERVED instead of UNSAFE (the default) as UNSAFE doesn't include /
401
+ url_encoded_app_uuid = URI.escape(app_uuid, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
402
+ begin
403
+ response = signed_mauth_connection.get("/mauth/#{@mauth_client.mauth_api_version}/security_tokens/#{url_encoded_app_uuid}.json")
404
+ rescue ::Faraday::Error::ConnectionFailed, ::Faraday::Error::TimeoutError
405
+ raise UnableToAuthenticateError, "mAuth service did not respond; received #{$!.class}: #{$!.message}"
406
+ end
407
+ if response.status == 200
408
+ begin
409
+ security_token = JSON.parse(response.body)
410
+ rescue JSON::ParserError
411
+ raise UnableToAuthenticateError, "mAuth service responded with unparseable json: #{response.body}\n#{$!.class}: #{$!.message}"
412
+ end
413
+ @cache_write_lock.synchronize do
414
+ @cache[app_uuid] = ExpirableSecurityToken.new(security_token, Time.now)
415
+ end
416
+ elsif response.status == 404
417
+ # signing with a key mAuth doesn't know about is considered inauthentic
418
+ raise InauthenticError, "mAuth service responded with 404 looking up public key for #{app_uuid}"
419
+ else
420
+ @mauth_client.send(:mauth_service_response_error, response)
421
+ end
422
+ end
423
+ @cache[app_uuid].security_token
424
+ end
425
+
426
+ private
427
+
428
+ def signed_mauth_connection
429
+ require 'faraday'
430
+ require 'mauth/faraday'
431
+ @mauth_client.faraday_options[:ssl] = { ca_path: @mauth_client.ssl_certs_path } if @mauth_client.ssl_certs_path
432
+ @signed_mauth_connection ||= ::Faraday.new(@mauth_client.mauth_baseurl, @mauth_client.faraday_options) do |builder|
433
+ builder.use MAuth::Faraday::MAuthClientUserAgent
434
+ builder.use MAuth::Faraday::RequestSigner, 'mauth_client' => @mauth_client
435
+ builder.adapter ::Faraday.default_adapter
436
+ end
437
+ end
438
+ end
439
+ end
440
+
441
+ # methods for remotely authenticating a request by sending it to the mauth service
442
+ module RemoteRequestAuthenticator
443
+ private
444
+
445
+ # takes an incoming request object (no support for responses currently), and errors if the
446
+ # object is not authentic according to its signature
447
+ def signature_valid!(object)
448
+ raise ArgumentError, "Remote Authenticator can only authenticate requests; received #{object.inspect}" unless object.is_a?(MAuth::Request)
449
+ authentication_ticket = {
450
+ 'verb' => object.attributes_for_signing[:verb],
451
+ 'app_uuid' => object.signature_app_uuid,
452
+ 'client_signature' => object.signature,
453
+ 'request_url' => object.attributes_for_signing[:request_url],
454
+ 'request_time' => object.x_mws_time,
455
+ 'b64encoded_body' => Base64.encode64(object.attributes_for_signing[:body] || '')
456
+ }
457
+ begin
458
+ response = mauth_connection.post("/mauth/#{mauth_api_version}/authentication_tickets.json", "authentication_ticket" => authentication_ticket)
459
+ rescue ::Faraday::Error::ConnectionFailed, ::Faraday::Error::TimeoutError
460
+ raise UnableToAuthenticateError, "mAuth service did not respond; received #{$!.class}: #{$!.message}"
461
+ end
462
+ if (200..299).cover?(response.status)
463
+ nil
464
+ elsif response.status == 412 || response.status == 404
465
+ # the mAuth service responds with 412 when the given request is not authentically signed.
466
+ # older versions of the mAuth service respond with 404 when the given app_uuid
467
+ # does not exist, which is also considered to not be authentically signed. newer
468
+ # versions of the service respond 412 in all cases, so the 404 check may be removed
469
+ # when the old version of the mAuth service is out of service.
470
+ raise InauthenticError, "The mAuth service responded with #{response.status}: #{response.body}"
471
+ else
472
+ mauth_service_response_error(response)
473
+ end
474
+ end
475
+
476
+ def mauth_connection
477
+ require 'faraday'
478
+ require 'faraday_middleware'
479
+ @mauth_connection ||= ::Faraday.new(mauth_baseurl, faraday_options) do |builder|
480
+ builder.use MAuth::Faraday::MAuthClientUserAgent
481
+ builder.use FaradayMiddleware::EncodeJson
482
+ builder.adapter ::Faraday.default_adapter
483
+ end
484
+ end
485
+ end
486
+ end
487
+ end
@@ -0,0 +1,7 @@
1
+ class Hash
2
+ # like stringify_keys, but does not attempt to stringify anything other than Symbols.
3
+ # other keys are left alone.
4
+ def stringify_symbol_keys
5
+ inject({}) { |acc, (k, v)| acc.update((k.is_a?(Symbol) ? k.to_s : k) => v) }
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ <%= warning.as_yaml_comment %>
2
+
3
+ MAUTH_CONF = MAuth::Client.default_config
4
+ require 'mauth/rack'
5
+ # ResponseSigner OPTIONAL; only use if you are registered in mauth service
6
+ Rails.application.config.middleware.insert_after Rack::Runtime, MAuth::Rack::ResponseSigner, MAUTH_CONF
7
+ if Rails.env.test? || Rails.env.development?
8
+ require 'mauth/fake/rack'
9
+ Rails.application.config.middleware.insert_after MAuth::Rack::ResponseSigner, MAuth::Rack::RequestAuthenticationFaker, MAUTH_CONF
10
+ else
11
+ Rails.application.config.middleware.insert_after MAuth::Rack::ResponseSigner, MAuth::Rack::RequestAuthenticatorNoAppStatus, MAUTH_CONF
12
+ end
@@ -0,0 +1,14 @@
1
+ <%= warning.as_yaml_comment %>
2
+
3
+ common: &common
4
+ mauth_baseurl: <%= configured.mauth_url! || 'http://localhost:7000' %>
5
+ mauth_api_version: v1
6
+ app_uuid: <%= configured.mauth_app_uuid! || 'fb17460e-9868-11e1-8399-0090f5ccb4d3' %>
7
+ private_key_file: config/mauth_key
8
+
9
+ production:
10
+ <<: *common
11
+ development:
12
+ <<: *common
13
+ test:
14
+ <<: *common
@@ -0,0 +1 @@
1
+ <%= ensure_is_private_key(configured.mauth_private_key! || generate_private_key.to_s) %>
@@ -0,0 +1,19 @@
1
+ require 'dice_bag'
2
+
3
+ class MauthTemplate < DiceBag::AvailableTemplates
4
+ def templates
5
+ ['mauth.yml.dice', 'mauth_key.dice'].map do |template|
6
+ File.join(File.dirname(__FILE__), template)
7
+ end
8
+ end
9
+ end
10
+
11
+ class MauthInitializerTemplate < DiceBag::AvailableTemplates
12
+ def templates_location
13
+ 'config/initializers'
14
+ end
15
+
16
+ def templates
17
+ [File.join(File.dirname(__FILE__), 'mauth.rb.dice')] if Object.const_defined?('Rails')
18
+ end
19
+ end
@@ -0,0 +1,45 @@
1
+ require 'mauth/rack'
2
+
3
+ module MAuth
4
+ module Rack
5
+ # This middleware bypasses actual authentication (it does not invoke mauth_client.authentic?). It
6
+ # instead uses a class attr method (is_authenic?) to determine if the request should be deemed authentic or not.
7
+ # Requests are authentic by default and RequestAuthenticationFaker.authentic = false must be called
8
+ # BEFORE EACH REQUEST in order to make a request inauthentic.
9
+ #
10
+ # This is for testing environments where you do not wish to rely on a mauth service for making requests.
11
+ #
12
+ # Note that if your application does not use env['mauth.app_uuid'] or env['mauth.authentic'] then it
13
+ # may be simpler to simply omit the request authentication middleware entirely in your test environment
14
+ # (rather than switching to this fake one), as all this does is add those keys to the request env.
15
+ class RequestAuthenticationFaker < MAuth::Rack::RequestAuthenticator
16
+ class << self
17
+ def is_authentic?
18
+ @is_authentic.nil? ? true : @is_authentic
19
+ end
20
+
21
+ def authentic=(is_auth = true)
22
+ @is_authentic = is_auth
23
+ end
24
+ end
25
+
26
+ def call(env)
27
+ retval = if should_authenticate?(env)
28
+ mauth_request = MAuth::Rack::Request.new(env)
29
+ if self.class.is_authentic?
30
+ @app.call(env.merge('mauth.app_uuid' => mauth_request.signature_app_uuid, 'mauth.authentic' => true))
31
+ else
32
+ response_for_inauthentic_request(env)
33
+ end
34
+ else
35
+ @app.call(env)
36
+ end
37
+
38
+ # ensure that the next request is marked authenic unless the consumer of this middleware explicitly deems otherwise
39
+ self.class.authentic = true
40
+
41
+ retval
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,87 @@
1
+ require 'mauth/middleware'
2
+ require 'mauth/request_and_response'
3
+
4
+ Faraday::Request.register_middleware(mauth_request_signer: proc { MAuth::Faraday::RequestSigner })
5
+ Faraday::Response.register_middleware(mauth_response_authenticator: proc { MAuth::Faraday::ResponseAuthenticator })
6
+
7
+ module MAuth
8
+ module Faraday
9
+ # faraday middleware to sign outgoing requests
10
+ class RequestSigner < MAuth::Middleware
11
+ def call(request_env)
12
+ signed_request_env = mauth_client.signed(MAuth::Faraday::Request.new(request_env)).request_env
13
+ @app.call(signed_request_env)
14
+ end
15
+ end
16
+
17
+ # faraday middleware to authenticate incoming responses
18
+ class ResponseAuthenticator < MAuth::Middleware
19
+ def call(request_env)
20
+ @app.call(request_env).on_complete do |response_env|
21
+ mauth_response = MAuth::Faraday::Response.new(response_env)
22
+ mauth_client.authenticate!(mauth_response) # raises MAuth::InauthenticError when inauthentic
23
+ response_env['mauth.app_uuid'] = mauth_response.signature_app_uuid
24
+ response_env['mauth.authentic'] = true
25
+ response_env
26
+ end
27
+ end
28
+ end
29
+
30
+ # representation of a request (outgoing) composed from a Faraday request env which can be
31
+ # passed to a Mauth::Client for signing
32
+ class Request < MAuth::Request
33
+ attr_reader :request_env
34
+ def initialize(request_env)
35
+ @request_env = request_env
36
+ end
37
+
38
+ def attributes_for_signing
39
+ request_url = @request_env[:url].path
40
+ request_url = '/' if request_url.empty?
41
+ @attributes_for_signing ||= { verb: @request_env[:method].to_s.upcase, request_url: request_url, body: @request_env[:body] }
42
+ end
43
+
44
+ # takes a Hash of headers; returns an instance of this class whose
45
+ # headers have been merged with the argument headers
46
+ def merge_headers(headers)
47
+ self.class.new(@request_env.merge(request_headers: @request_env[:request_headers].merge(headers)))
48
+ end
49
+ end
50
+
51
+ # representation of a Response (incoming) composed from a Faraday response env which can be
52
+ # passed to a Mauth::Client for authentication
53
+ class Response < MAuth::Response
54
+ include Signed
55
+ attr_reader :response_env
56
+ def initialize(response_env)
57
+ @response_env = response_env
58
+ end
59
+
60
+ def attributes_for_signing
61
+ @attributes_for_signing ||= { status_code: response_env[:status], body: response_env[:body] }
62
+ end
63
+
64
+ def x_mws_time
65
+ @response_env[:response_headers]['x-mws-time']
66
+ end
67
+
68
+ def x_mws_authentication
69
+ @response_env[:response_headers]['x-mws-authentication']
70
+ end
71
+ end
72
+
73
+ # add MAuth-Client's user-agent to a request
74
+ class MAuthClientUserAgent
75
+ def initialize(app, agent_base = "Mauth-Client")
76
+ @app = app
77
+ @agent_base = agent_base
78
+ end
79
+
80
+ def call(request_env)
81
+ agent = "#{@agent_base} (MAuth-Client: #{MAuth::VERSION}; Ruby: #{RUBY_VERSION}; platform: #{RUBY_PLATFORM})"
82
+ request_env[:request_headers]['User-Agent'] ||= agent
83
+ @app.call(request_env)
84
+ end
85
+ end
86
+ end
87
+ end