mauth-client 4.2.1 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +5 -9
- data/CHANGELOG.md +5 -5
- data/CONTRIBUTING.md +8 -0
- data/README.md +18 -6
- data/Rakefile +107 -0
- data/exe/mauth-client +19 -19
- data/lib/mauth/client/authenticator_base.rb +118 -0
- data/lib/mauth/client/local_authenticator.rb +137 -0
- data/lib/mauth/client/remote_authenticator.rb +75 -0
- data/lib/mauth/client/security_token_cacher.rb +71 -0
- data/lib/mauth/client/signer.rb +67 -0
- data/lib/mauth/client.rb +99 -366
- data/lib/mauth/dice_bag/mauth.yml.dice +2 -0
- data/lib/mauth/errors.rb +29 -0
- data/lib/mauth/fake/rack.rb +3 -1
- data/lib/mauth/faraday.rb +17 -3
- data/lib/mauth/rack.rb +60 -16
- data/lib/mauth/request_and_response.rb +115 -8
- data/lib/mauth/version.rb +1 -1
- data/mauth-client.gemspec +3 -3
- metadata +29 -41
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 '
|
11
|
-
require 'mauth/
|
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
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
36
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
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
|
-
|
262
|
-
|
263
|
-
|
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(
|
415
|
-
UnableToAuthenticateError.new("Cannot fetch public keys from mAuth service without a private key!")
|
416
|
-
)
|
417
|
-
end
|
418
|
-
|
419
|
-
def get(app_uuid)
|
420
|
-
# url-encode the app_uuid to prevent trickery like escaping upward with ../../ in a malicious
|
421
|
-
# app_uuid - probably not exploitable, but this is the right way to do it anyway.
|
422
|
-
# use UNRESERVED instead of UNSAFE (the default) as UNSAFE doesn't include /
|
423
|
-
url_encoded_app_uuid = URI.escape(app_uuid, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
424
|
-
path = "/mauth/#{@mauth_client.mauth_api_version}/security_tokens/#{url_encoded_app_uuid}.json"
|
425
|
-
response = signed_mauth_connection.get(path)
|
426
|
-
|
427
|
-
case response.status
|
428
|
-
when 200
|
429
|
-
security_token_from(response.body)
|
430
|
-
when 404
|
431
|
-
# signing with a key mAuth doesn't know about is considered inauthentic
|
432
|
-
raise InauthenticError, "mAuth service responded with 404 looking up public key for #{app_uuid}"
|
433
|
-
else
|
434
|
-
@mauth_client.send(:mauth_service_response_error, response)
|
435
|
-
end
|
436
|
-
rescue ::Faraday::ConnectionFailed, ::Faraday::TimeoutError => e
|
437
|
-
msg = "mAuth service did not respond; received #{e.class}: #{e.message}"
|
438
|
-
@mauth_client.logger.error("Unable to authenticate with MAuth. Exception #{msg}")
|
439
|
-
raise UnableToAuthenticateError, msg
|
440
|
-
end
|
441
|
-
|
442
|
-
private
|
443
|
-
|
444
|
-
def security_token_from(response_body)
|
445
|
-
JSON.parse response_body
|
446
|
-
rescue JSON::ParserError => e
|
447
|
-
msg = "mAuth service responded with unparseable json: #{response_body}\n#{e.class}: #{e.message}"
|
448
|
-
@mauth_client.logger.error("Unable to authenticate with MAuth. Exception #{msg}")
|
449
|
-
raise UnableToAuthenticateError, msg
|
450
|
-
end
|
451
|
-
|
452
|
-
def signed_mauth_connection
|
453
|
-
@signed_mauth_connection ||= begin
|
454
|
-
if @mauth_client.ssl_certs_path
|
455
|
-
@mauth_client.faraday_options[:ssl] = { ca_path: @mauth_client.ssl_certs_path }
|
456
|
-
end
|
231
|
+
module ConfigFile
|
232
|
+
GITHUB_URL = 'https://github.com/mdsol/mauth-client-ruby'.freeze
|
233
|
+
@config = {}
|
457
234
|
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
builder.use :http_cache, logger: MAuth::Client.new.logger, shared_cache: false
|
462
|
-
builder.adapter ::Faraday.default_adapter
|
463
|
-
end
|
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
|
-
|
470
|
-
|
471
|
-
|
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
|
-
|
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
|
@@ -5,6 +5,8 @@ common: &common
|
|
5
5
|
mauth_api_version: v1
|
6
6
|
app_uuid: <%= configured.mauth_app_uuid! || 'fb17460e-9868-11e1-8399-0090f5ccb4d3' %>
|
7
7
|
private_key_file: config/mauth_key
|
8
|
+
v2_only_authenticate: <%= configured.v2_only_authenticate || 'false' %>
|
9
|
+
v2_only_sign_requests: <%= configured.v2_only_sign_requests || 'false' %>
|
8
10
|
|
9
11
|
production:
|
10
12
|
<<: *common
|
data/lib/mauth/errors.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module MAuth
|
2
|
+
# mAuth client was unable to verify the authenticity of a signed object (this does NOT mean the
|
3
|
+
# object is inauthentic). typically due to a failure communicating with the mAuth service, in
|
4
|
+
# which case the error may include the attribute mauth_service_response - a response from
|
5
|
+
# the mauth service (if it was contactable at all), which may contain more information about
|
6
|
+
# the error.
|
7
|
+
class UnableToAuthenticateError < StandardError
|
8
|
+
# the response from the MAuth service encountered when attempting to retrieve authentication
|
9
|
+
attr_accessor :mauth_service_response
|
10
|
+
end
|
11
|
+
|
12
|
+
# used to indicate that an object was expected to be validly signed but its signature does not
|
13
|
+
# match its contents, and so is inauthentic.
|
14
|
+
class InauthenticError < StandardError; end
|
15
|
+
|
16
|
+
# Used when the incoming request does not contain any mAuth related information
|
17
|
+
class MAuthNotPresent < StandardError; end
|
18
|
+
|
19
|
+
# required information for signing was missing
|
20
|
+
class UnableToSignError < StandardError; end
|
21
|
+
|
22
|
+
# used when an object has the V1 headers but not the V2 headers and the
|
23
|
+
# V2_ONLY_AUTHENTICATE variable is set to true.
|
24
|
+
class MissingV2Error < StandardError; end
|
25
|
+
|
26
|
+
class Client
|
27
|
+
class ConfigurationError < StandardError; end
|
28
|
+
end
|
29
|
+
end
|
data/lib/mauth/fake/rack.rb
CHANGED
@@ -26,8 +26,10 @@ module MAuth
|
|
26
26
|
def call(env)
|
27
27
|
retval = if should_authenticate?(env)
|
28
28
|
mauth_request = MAuth::Rack::Request.new(env)
|
29
|
+
env['mauth.protocol_version'] = mauth_request.protocol_version
|
30
|
+
|
29
31
|
if self.class.is_authentic?
|
30
|
-
@app.call(env.merge('mauth.app_uuid' => mauth_request.signature_app_uuid, 'mauth.authentic' => true))
|
32
|
+
@app.call(env.merge!('mauth.app_uuid' => mauth_request.signature_app_uuid, 'mauth.authentic' => true))
|
31
33
|
else
|
32
34
|
response_for_inauthentic_request(env)
|
33
35
|
end
|
data/lib/mauth/faraday.rb
CHANGED
@@ -36,9 +36,15 @@ module MAuth
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def attributes_for_signing
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
@attributes_for_signing ||= begin
|
40
|
+
request_url = @request_env[:url].path.empty? ? '/' : @request_env[:url].path
|
41
|
+
{
|
42
|
+
verb: @request_env[:method].to_s.upcase,
|
43
|
+
request_url: request_url,
|
44
|
+
body: @request_env[:body],
|
45
|
+
query_string: @request_env[:url].query
|
46
|
+
}
|
47
|
+
end
|
42
48
|
end
|
43
49
|
|
44
50
|
# takes a Hash of headers; returns an instance of this class whose
|
@@ -68,6 +74,14 @@ module MAuth
|
|
68
74
|
def x_mws_authentication
|
69
75
|
@response_env[:response_headers]['x-mws-authentication']
|
70
76
|
end
|
77
|
+
|
78
|
+
def mcc_time
|
79
|
+
@response_env[:response_headers]['mcc-time']
|
80
|
+
end
|
81
|
+
|
82
|
+
def mcc_authentication
|
83
|
+
@response_env[:response_headers]['mcc-authentication']
|
84
|
+
end
|
71
85
|
end
|
72
86
|
|
73
87
|
# add MAuth-Client's user-agent to a request
|