mauth-client 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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