mauth-client 4.2.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/.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
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/.travis.yml
CHANGED
@@ -13,23 +13,19 @@ before_install:
|
|
13
13
|
|
14
14
|
install:
|
15
15
|
- bundle install --jobs=3 --retry=3
|
16
|
-
-
|
17
|
-
curl -H 'Cache-Control: no-cache'
|
18
|
-
https://raw.githubusercontent.com/mdsol/fossa_ci_scripts/main/travis_ci/fossa_install.sh |
|
19
|
-
bash -s -- -b $TRAVIS_BUILD_DIR
|
16
|
+
- |-
|
17
|
+
curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/mdsol/fossa_ci_scripts/master/travis_ci/fossa_install.sh | bash -s -- -b $TRAVIS_BUILD_DIR
|
20
18
|
|
21
19
|
script:
|
22
20
|
- bundle exec rspec
|
23
|
-
-
|
24
|
-
curl -H 'Cache-Control: no-cache'
|
25
|
-
https://raw.githubusercontent.com/mdsol/fossa_ci_scripts/main/travis_ci/fossa_run.sh |
|
26
|
-
bash -s -- -b $TRAVIS_BUILD_DIR
|
21
|
+
- |-
|
22
|
+
curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/mdsol/fossa_ci_scripts/master/travis_ci/fossa_run.sh | bash -s -- -b $TRAVIS_BUILD_DIR
|
27
23
|
|
28
24
|
deploy:
|
29
25
|
provider: rubygems
|
30
26
|
gem: mauth-client
|
31
27
|
api_key:
|
32
|
-
secure:
|
28
|
+
secure: J0aPDp4+Ev2L+ZDcgpF+hAG95S4IsD6pCiDRxDWnrk79P5hq1rXoD3S39ANyqtQEQqkoVjsgoSP5JLi420aL2lYj7mhvaEOty9fK+flwUhI4nw+Gztm7EKNDNX8WKvk4fl4Zc7noIeI0uyes867hDjRQfyYvUuma7aK5H9NWzNUV9Q+KrVAoneVDGnNydxwkuuIpOFdjbVQgNpxVhVBV7Q4OLsB1KtWB9lptMwhqnyqZKex7JZ+37sojaj3oVT5ijrnAm+bR1QO1hGIOwuBako2iz+MBZHPccM4BEFsZme/7olypxv0JfeCuhqDnH1VWIFh6IZRDeLnZuX3qOhkdx4HLwxB//5O5+iapK0wh1zbnLvXqkE1dalUHyaZzStKH9xchIWl5I77Ica232OJYrpj9hhroae0p3VARF0IoZceKaH8NnMpq+nBAW4REcWrqPpe9xkRLDTNibkpaAy08vGOF2kPZkWw4lfkVBM1+wjY2xDn6wJ7VgQ1BeosbeTXbmny2TUeI22beihn894tzpCPPHiTRvKu0lV3jBfeoOAXzE333PrGm3zF9MDhg+1/iBwXVhdoOwEwBPQ/3Hu37xJn0AfRneni4StYnIkZ1Ur9Vub03J/3C3Aw6it99rQSWvC+2PzHqQhsG22VprvxlozFe1jFzdqKgvDkbkn44ltI=
|
33
29
|
on:
|
34
30
|
tags: true
|
35
31
|
repo: mdsol/mauth-client-ruby
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
##
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
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
6
|
|
7
7
|
## v4.1.1
|
8
8
|
- Use warning level instead of error level for logs about missing mauth header.
|
data/CONTRIBUTING.md
CHANGED
@@ -18,3 +18,11 @@ Next, run the tests:
|
|
18
18
|
```
|
19
19
|
bundle exec rspec
|
20
20
|
```
|
21
|
+
|
22
|
+
## Running Benchmark
|
23
|
+
|
24
|
+
If you make changes which could affect performance, please run the benchmark before and after the change as a sanity check.
|
25
|
+
|
26
|
+
```
|
27
|
+
bundle exec rake benchmark
|
28
|
+
```
|
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
# MAuth
|
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 }
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# methods common to RemoteRequestAuthenticator and LocalAuthenticator
|
2
|
+
|
3
|
+
module MAuth
|
4
|
+
class Client
|
5
|
+
module AuthenticatorBase
|
6
|
+
ALLOWED_DRIFT_SECONDS = 300
|
7
|
+
|
8
|
+
# takes an incoming request or response object, and returns whether
|
9
|
+
# the object is authentic according to its signature.
|
10
|
+
def authentic?(object)
|
11
|
+
log_authentication_request(object)
|
12
|
+
begin
|
13
|
+
authenticate!(object)
|
14
|
+
true
|
15
|
+
rescue InauthenticError, MAuthNotPresent, MissingV2Error
|
16
|
+
false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# raises InauthenticError unless the given object is authentic. Will only
|
21
|
+
# authenticate with v2 if the environment variable V2_ONLY_AUTHENTICATE
|
22
|
+
# is set. Otherwise will authenticate with only the highest protocol version present
|
23
|
+
def authenticate!(object)
|
24
|
+
if object.protocol_version == 2
|
25
|
+
authenticate_v2!(object)
|
26
|
+
elsif object.protocol_version == 1
|
27
|
+
if v2_only_authenticate?
|
28
|
+
# If v2 is required but not present and v1 is present we raise MissingV2Error
|
29
|
+
msg = 'This service requires mAuth v2 mcc-authentication header but only v1 x-mws-authentication is present'
|
30
|
+
logger.error(msg)
|
31
|
+
raise MissingV2Error, msg
|
32
|
+
end
|
33
|
+
|
34
|
+
authenticate_v1!(object)
|
35
|
+
else
|
36
|
+
sub_str = v2_only_authenticate? ? '' : 'X-MWS-Authentication header is blank, '
|
37
|
+
msg = "Authentication Failed. No mAuth signature present; #{sub_str}MCC-Authentication header is blank."
|
38
|
+
logger.warn("mAuth signature not present on #{object.class}. Exception: #{msg}")
|
39
|
+
raise MAuthNotPresent, msg
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Note: This log is likely consumed downstream and the contents SHOULD NOT
|
46
|
+
# be changed without a thorough review of downstream consumers.
|
47
|
+
def log_authentication_request(object)
|
48
|
+
object_app_uuid = object.signature_app_uuid || '[none provided]'
|
49
|
+
object_token = object.signature_token || '[none provided]'
|
50
|
+
logger.info(
|
51
|
+
"Mauth-client attempting to authenticate request from app with mauth" \
|
52
|
+
" app uuid #{object_app_uuid} to app with mauth app uuid #{client_app_uuid}" \
|
53
|
+
" using version #{object_token}."
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def log_inauthentic(object, message)
|
58
|
+
logger.error("mAuth signature authentication failed for #{object.class}. Exception: #{message}")
|
59
|
+
end
|
60
|
+
|
61
|
+
def time_within_valid_range!(object, time_signed, now = Time.now)
|
62
|
+
return if (-ALLOWED_DRIFT_SECONDS..ALLOWED_DRIFT_SECONDS).cover?(now.to_i - time_signed)
|
63
|
+
|
64
|
+
msg = "Time verification failed. #{time_signed} not within #{ALLOWED_DRIFT_SECONDS} of #{now}"
|
65
|
+
log_inauthentic(object, msg)
|
66
|
+
raise InauthenticError, msg
|
67
|
+
end
|
68
|
+
|
69
|
+
# V1 helpers
|
70
|
+
def authenticate_v1!(object)
|
71
|
+
time_valid_v1!(object)
|
72
|
+
token_valid_v1!(object)
|
73
|
+
signature_valid_v1!(object)
|
74
|
+
end
|
75
|
+
|
76
|
+
def time_valid_v1!(object)
|
77
|
+
if object.x_mws_time.nil?
|
78
|
+
msg = 'Time verification failed. No x-mws-time present.'
|
79
|
+
log_inauthentic(object, msg)
|
80
|
+
raise InauthenticError, msg
|
81
|
+
end
|
82
|
+
time_within_valid_range!(object, object.x_mws_time.to_i)
|
83
|
+
end
|
84
|
+
|
85
|
+
def token_valid_v1!(object)
|
86
|
+
return if object.signature_token == MWS_TOKEN
|
87
|
+
|
88
|
+
msg = "Token verification failed. Expected #{MWS_TOKEN}; token was #{object.signature_token}"
|
89
|
+
log_inauthentic(object, msg)
|
90
|
+
raise InauthenticError, msg
|
91
|
+
end
|
92
|
+
|
93
|
+
# V2 helpers
|
94
|
+
def authenticate_v2!(object)
|
95
|
+
time_valid_v2!(object)
|
96
|
+
token_valid_v2!(object)
|
97
|
+
signature_valid_v2!(object)
|
98
|
+
end
|
99
|
+
|
100
|
+
def time_valid_v2!(object)
|
101
|
+
if object.mcc_time.nil?
|
102
|
+
msg = 'Time verification failed. No MCC-Time present.'
|
103
|
+
log_inauthentic(object, msg)
|
104
|
+
raise InauthenticError, msg
|
105
|
+
end
|
106
|
+
time_within_valid_range!(object, object.mcc_time.to_i)
|
107
|
+
end
|
108
|
+
|
109
|
+
def token_valid_v2!(object)
|
110
|
+
return if object.signature_token == MWSV2_TOKEN
|
111
|
+
|
112
|
+
msg = "Token verification failed. Expected #{MWSV2_TOKEN}; token was #{object.signature_token}"
|
113
|
+
log_inauthentic(object, msg)
|
114
|
+
raise InauthenticError, msg
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'mauth/client/security_token_cacher'
|
2
|
+
require 'mauth/client/signer'
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
# methods to verify the authenticity of signed requests and responses locally, retrieving
|
6
|
+
# public keys from the mAuth service as needed
|
7
|
+
|
8
|
+
module MAuth
|
9
|
+
class Client
|
10
|
+
module LocalAuthenticator
|
11
|
+
private
|
12
|
+
|
13
|
+
def signature_valid_v1!(object)
|
14
|
+
# We are in an unfortunate situation in which Euresource is percent-encoding parts of paths, but not
|
15
|
+
# all of them. In particular, Euresource is percent-encoding all special characters save for '/'.
|
16
|
+
# Also, unfortunately, Nginx unencodes URIs before sending them off to served applications, though
|
17
|
+
# other web servers (particularly those we typically use for local testing) do not. The various forms
|
18
|
+
# of the expected string to sign are meant to cover the main cases.
|
19
|
+
# TODO: Revisit and simplify this unfortunate situation.
|
20
|
+
|
21
|
+
original_request_uri = object.attributes_for_signing[:request_url]
|
22
|
+
|
23
|
+
# craft an expected string-to-sign without doing any percent-encoding
|
24
|
+
expected_no_reencoding = object.string_to_sign_v1(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
|
25
|
+
|
26
|
+
# do a simple percent reencoding variant of the path
|
27
|
+
object.attributes_for_signing[:request_url] = CGI.escape(original_request_uri.to_s)
|
28
|
+
expected_for_percent_reencoding = object.string_to_sign_v1(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
|
29
|
+
|
30
|
+
# do a moderately complex Euresource-style reencoding of the path
|
31
|
+
object.attributes_for_signing[:request_url] = euresource_escape(original_request_uri.to_s)
|
32
|
+
expected_euresource_style_reencoding = object.string_to_sign_v1(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
|
33
|
+
|
34
|
+
# reset the object original request_uri, just in case we need it again
|
35
|
+
object.attributes_for_signing[:request_url] = original_request_uri
|
36
|
+
|
37
|
+
begin
|
38
|
+
pubkey = OpenSSL::PKey::RSA.new(retrieve_public_key(object.signature_app_uuid))
|
39
|
+
actual = pubkey.public_decrypt(Base64.decode64(object.signature))
|
40
|
+
rescue OpenSSL::PKey::PKeyError => e
|
41
|
+
msg = "Public key decryption of signature failed! #{e.class}: #{e.message}"
|
42
|
+
log_inauthentic(object, msg)
|
43
|
+
raise InauthenticError, msg
|
44
|
+
end
|
45
|
+
|
46
|
+
unless verify_signature_v1!(actual, expected_no_reencoding) ||
|
47
|
+
verify_signature_v1!(actual, expected_euresource_style_reencoding) ||
|
48
|
+
verify_signature_v1!(actual, expected_for_percent_reencoding)
|
49
|
+
msg = "Signature verification failed for #{object.class}"
|
50
|
+
log_inauthentic(object, msg)
|
51
|
+
raise InauthenticError, msg
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def verify_signature_v1!(actual, expected_str_to_sign)
|
56
|
+
actual == Digest::SHA512.hexdigest(expected_str_to_sign)
|
57
|
+
end
|
58
|
+
|
59
|
+
def signature_valid_v2!(object)
|
60
|
+
# We are in an unfortunate situation in which Euresource is percent-encoding parts of paths, but not
|
61
|
+
# all of them. In particular, Euresource is percent-encoding all special characters save for '/'.
|
62
|
+
# Also, unfortunately, Nginx unencodes URIs before sending them off to served applications, though
|
63
|
+
# other web servers (particularly those we typically use for local testing) do not. The various forms
|
64
|
+
# of the expected string to sign are meant to cover the main cases.
|
65
|
+
# TODO: Revisit and simplify this unfortunate situation.
|
66
|
+
|
67
|
+
original_request_uri = object.attributes_for_signing[:request_url]
|
68
|
+
original_query_string = object.attributes_for_signing[:query_string]
|
69
|
+
|
70
|
+
# craft an expected string-to-sign without doing any percent-encoding
|
71
|
+
expected_no_reencoding = object.string_to_sign_v2(
|
72
|
+
time: object.mcc_time,
|
73
|
+
app_uuid: object.signature_app_uuid
|
74
|
+
)
|
75
|
+
|
76
|
+
# do a simple percent reencoding variant of the path
|
77
|
+
expected_for_percent_reencoding = object.string_to_sign_v2(
|
78
|
+
time: object.mcc_time,
|
79
|
+
app_uuid: object.signature_app_uuid,
|
80
|
+
request_url: CGI.escape(original_request_uri.to_s),
|
81
|
+
query_string: CGI.escape(original_query_string.to_s)
|
82
|
+
)
|
83
|
+
|
84
|
+
# do a moderately complex Euresource-style reencoding of the path
|
85
|
+
expected_euresource_style_reencoding = object.string_to_sign_v2(
|
86
|
+
time: object.mcc_time,
|
87
|
+
app_uuid: object.signature_app_uuid,
|
88
|
+
request_url: euresource_escape(original_request_uri.to_s),
|
89
|
+
query_string: euresource_escape(original_query_string.to_s)
|
90
|
+
)
|
91
|
+
|
92
|
+
pubkey = OpenSSL::PKey::RSA.new(retrieve_public_key(object.signature_app_uuid))
|
93
|
+
actual = Base64.decode64(object.signature)
|
94
|
+
|
95
|
+
unless verify_signature_v2!(object, actual, pubkey, expected_no_reencoding) ||
|
96
|
+
verify_signature_v2!(object, actual, pubkey, expected_euresource_style_reencoding) ||
|
97
|
+
verify_signature_v2!(object, actual, pubkey, expected_for_percent_reencoding)
|
98
|
+
msg = "Signature inauthentic for #{object.class}"
|
99
|
+
log_inauthentic(object, msg)
|
100
|
+
raise InauthenticError, msg
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def verify_signature_v2!(object, actual, pubkey, expected_str_to_sign)
|
105
|
+
pubkey.verify(
|
106
|
+
MAuth::Client::SIGNING_DIGEST,
|
107
|
+
actual,
|
108
|
+
expected_str_to_sign
|
109
|
+
)
|
110
|
+
rescue OpenSSL::PKey::PKeyError => e
|
111
|
+
msg = "RSA verification of signature failed! #{e.class}: #{e.message}"
|
112
|
+
log_inauthentic(object, msg)
|
113
|
+
raise InauthenticError, msg
|
114
|
+
end
|
115
|
+
|
116
|
+
# Note: RFC 3986 (https://www.ietf.org/rfc/rfc3986.txt) reserves the forward slash "/"
|
117
|
+
# and number sign "#" as component delimiters. Since these are valid URI components,
|
118
|
+
# they are decoded back into characters here to avoid signature invalidation
|
119
|
+
def euresource_escape(str)
|
120
|
+
CGI.escape(str).gsub(/%2F|%23/, '%2F' => '/', '%23' => '#')
|
121
|
+
end
|
122
|
+
|
123
|
+
def retrieve_public_key(app_uuid)
|
124
|
+
retrieve_security_token(app_uuid)['security_token']['public_key_str']
|
125
|
+
end
|
126
|
+
|
127
|
+
def retrieve_security_token(app_uuid)
|
128
|
+
security_token_cacher.get(app_uuid)
|
129
|
+
end
|
130
|
+
|
131
|
+
def security_token_cacher
|
132
|
+
@security_token_cacher ||= SecurityTokenCacher.new(self)
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|