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