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
@@ -0,0 +1,75 @@
|
|
1
|
+
# methods for remotely authenticating a request by sending it to the mauth service
|
2
|
+
|
3
|
+
module MAuth
|
4
|
+
class Client
|
5
|
+
module RemoteRequestAuthenticator
|
6
|
+
private
|
7
|
+
|
8
|
+
# takes an incoming request object (no support for responses currently), and errors if the
|
9
|
+
# object is not authentic according to its signature
|
10
|
+
def signature_valid_v1!(object)
|
11
|
+
raise ArgumentError, "Remote Authenticator can only authenticate requests; received #{object.inspect}" unless object.is_a?(MAuth::Request)
|
12
|
+
authentication_ticket = {
|
13
|
+
'verb' => object.attributes_for_signing[:verb],
|
14
|
+
'app_uuid' => object.signature_app_uuid,
|
15
|
+
'client_signature' => object.signature,
|
16
|
+
'request_url' => object.attributes_for_signing[:request_url],
|
17
|
+
'request_time' => object.x_mws_time,
|
18
|
+
'b64encoded_body' => Base64.encode64(object.attributes_for_signing[:body] || '')
|
19
|
+
}
|
20
|
+
make_mauth_request(authentication_ticket)
|
21
|
+
end
|
22
|
+
|
23
|
+
def signature_valid_v2!(object)
|
24
|
+
unless object.is_a?(MAuth::Request)
|
25
|
+
msg = "Remote Authenticator can only authenticate requests; received #{object.inspect}"
|
26
|
+
raise ArgumentError, msg
|
27
|
+
end
|
28
|
+
|
29
|
+
authentication_ticket = {
|
30
|
+
verb: object.attributes_for_signing[:verb],
|
31
|
+
app_uuid: object.signature_app_uuid,
|
32
|
+
client_signature: object.signature,
|
33
|
+
request_url: object.attributes_for_signing[:request_url],
|
34
|
+
request_time: object.mcc_time,
|
35
|
+
b64encoded_body: Base64.encode64(object.attributes_for_signing[:body] || ''),
|
36
|
+
query_string: object.attributes_for_signing[:query_string],
|
37
|
+
token: object.signature_token
|
38
|
+
}
|
39
|
+
make_mauth_request(authentication_ticket)
|
40
|
+
end
|
41
|
+
|
42
|
+
def make_mauth_request(authentication_ticket)
|
43
|
+
begin
|
44
|
+
response = mauth_connection.post("/mauth/#{mauth_api_version}/authentication_tickets.json", 'authentication_ticket' => authentication_ticket)
|
45
|
+
rescue ::Faraday::Error::ConnectionFailed, ::Faraday::Error::TimeoutError => e
|
46
|
+
msg = "mAuth service did not respond; received #{e.class}: #{e.message}"
|
47
|
+
logger.error("Unable to authenticate with MAuth. Exception #{msg}")
|
48
|
+
raise UnableToAuthenticateError, msg
|
49
|
+
end
|
50
|
+
if (200..299).cover?(response.status)
|
51
|
+
nil
|
52
|
+
elsif response.status == 412 || response.status == 404
|
53
|
+
# the mAuth service responds with 412 when the given request is not authentically signed.
|
54
|
+
# older versions of the mAuth service respond with 404 when the given app_uuid
|
55
|
+
# does not exist, which is also considered to not be authentically signed. newer
|
56
|
+
# versions of the service respond 412 in all cases, so the 404 check may be removed
|
57
|
+
# when the old version of the mAuth service is out of service.
|
58
|
+
raise InauthenticError, "The mAuth service responded with #{response.status}: #{response.body}"
|
59
|
+
else
|
60
|
+
mauth_service_response_error(response)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def mauth_connection
|
65
|
+
require 'faraday'
|
66
|
+
require 'faraday_middleware'
|
67
|
+
@mauth_connection ||= ::Faraday.new(mauth_baseurl, faraday_options) do |builder|
|
68
|
+
builder.use MAuth::Faraday::MAuthClientUserAgent
|
69
|
+
builder.use FaradayMiddleware::EncodeJson
|
70
|
+
builder.adapter ::Faraday.default_adapter
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module MAuth
|
2
|
+
class Client
|
3
|
+
module LocalAuthenticator
|
4
|
+
class SecurityTokenCacher
|
5
|
+
|
6
|
+
class ExpirableSecurityToken < Struct.new(:security_token, :create_time)
|
7
|
+
CACHE_LIFE = 60
|
8
|
+
def expired?
|
9
|
+
create_time + CACHE_LIFE < Time.now
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(mauth_client)
|
14
|
+
@mauth_client = mauth_client
|
15
|
+
# TODO: should this be UnableToSignError?
|
16
|
+
@mauth_client.assert_private_key(UnableToAuthenticateError.new("Cannot fetch public keys from mAuth service without a private key!"))
|
17
|
+
@cache = {}
|
18
|
+
require 'thread'
|
19
|
+
@cache_write_lock = Mutex.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def get(app_uuid)
|
23
|
+
if !@cache[app_uuid] || @cache[app_uuid].expired?
|
24
|
+
# url-encode the app_uuid to prevent trickery like escaping upward with ../../ in a malicious
|
25
|
+
# app_uuid - probably not exploitable, but this is the right way to do it anyway.
|
26
|
+
# use UNRESERVED instead of UNSAFE (the default) as UNSAFE doesn't include /
|
27
|
+
url_encoded_app_uuid = URI.escape(app_uuid, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
28
|
+
begin
|
29
|
+
response = signed_mauth_connection.get("/mauth/#{@mauth_client.mauth_api_version}/security_tokens/#{url_encoded_app_uuid}.json")
|
30
|
+
rescue ::Faraday::Error::ConnectionFailed, ::Faraday::Error::TimeoutError => e
|
31
|
+
msg = "mAuth service did not respond; received #{e.class}: #{e.message}"
|
32
|
+
@mauth_client.logger.error("Unable to authenticate with MAuth. Exception #{msg}")
|
33
|
+
raise UnableToAuthenticateError, msg
|
34
|
+
end
|
35
|
+
if response.status == 200
|
36
|
+
begin
|
37
|
+
security_token = JSON.parse(response.body)
|
38
|
+
rescue JSON::ParserError => e
|
39
|
+
msg = "mAuth service responded with unparseable json: #{response.body}\n#{e.class}: #{e.message}"
|
40
|
+
@mauth_client.logger.error("Unable to authenticate with MAuth. Exception #{msg}")
|
41
|
+
raise UnableToAuthenticateError, msg
|
42
|
+
end
|
43
|
+
@cache_write_lock.synchronize do
|
44
|
+
@cache[app_uuid] = ExpirableSecurityToken.new(security_token, Time.now)
|
45
|
+
end
|
46
|
+
elsif response.status == 404
|
47
|
+
# signing with a key mAuth doesn't know about is considered inauthentic
|
48
|
+
raise InauthenticError, "mAuth service responded with 404 looking up public key for #{app_uuid}"
|
49
|
+
else
|
50
|
+
@mauth_client.send(:mauth_service_response_error, response)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
@cache[app_uuid].security_token
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def signed_mauth_connection
|
59
|
+
require 'faraday'
|
60
|
+
require 'mauth/faraday'
|
61
|
+
@mauth_client.faraday_options[:ssl] = { ca_path: @mauth_client.ssl_certs_path } if @mauth_client.ssl_certs_path
|
62
|
+
@signed_mauth_connection ||= ::Faraday.new(@mauth_client.mauth_baseurl, @mauth_client.faraday_options) do |builder|
|
63
|
+
builder.use MAuth::Faraday::MAuthClientUserAgent
|
64
|
+
builder.use MAuth::Faraday::RequestSigner, 'mauth_client' => @mauth_client
|
65
|
+
builder.adapter ::Faraday.default_adapter
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'mauth/errors'
|
3
|
+
|
4
|
+
# methods to sign requests and responses. part of MAuth::Client
|
5
|
+
|
6
|
+
module MAuth
|
7
|
+
class Client
|
8
|
+
SIGNING_DIGEST = OpenSSL::Digest::SHA512.new
|
9
|
+
|
10
|
+
module Signer
|
11
|
+
UNABLE_TO_SIGN_ERR = UnableToSignError.new('mAuth client cannot sign without a private key!')
|
12
|
+
|
13
|
+
# takes an outgoing request or response object, and returns an object of the same class
|
14
|
+
# whose headers are updated to include mauth's signature headers
|
15
|
+
def signed(object, attributes = {})
|
16
|
+
object.merge_headers(signed_headers(object, attributes))
|
17
|
+
end
|
18
|
+
|
19
|
+
# signs with v1 only. used when signing responses to v1 requests.
|
20
|
+
def signed_v1(object, attributes = {})
|
21
|
+
object.merge_headers(signed_headers_v1(object, attributes))
|
22
|
+
end
|
23
|
+
|
24
|
+
def signed_v2(object, attributes = {})
|
25
|
+
object.merge_headers(signed_headers_v2(object, attributes))
|
26
|
+
end
|
27
|
+
|
28
|
+
# takes a signable object (outgoing request or response). returns a hash of headers to be
|
29
|
+
# applied to the object which comprises its signature.
|
30
|
+
def signed_headers(object, attributes = {})
|
31
|
+
if v2_only_sign_requests?
|
32
|
+
signed_headers_v2(object, attributes)
|
33
|
+
else # by default sign with both the v1 and v2 protocol
|
34
|
+
signed_headers_v1(object, attributes).merge(signed_headers_v2(object, attributes))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def signed_headers_v1(object, attributes = {})
|
39
|
+
attributes = { time: Time.now.to_i.to_s, app_uuid: client_app_uuid }.merge(attributes)
|
40
|
+
string_to_sign = object.string_to_sign_v1(attributes)
|
41
|
+
signature = self.signature_v1(string_to_sign)
|
42
|
+
{ 'X-MWS-Authentication' => "#{MWS_TOKEN} #{client_app_uuid}:#{signature}", 'X-MWS-Time' => attributes[:time] }
|
43
|
+
end
|
44
|
+
|
45
|
+
def signed_headers_v2(object, attributes = {})
|
46
|
+
attributes = { time: Time.now.to_i.to_s, app_uuid: client_app_uuid }.merge(attributes)
|
47
|
+
string_to_sign = object.string_to_sign_v2(attributes)
|
48
|
+
signature = self.signature_v2(string_to_sign)
|
49
|
+
{
|
50
|
+
'MCC-Authentication' => "#{MWSV2_TOKEN} #{client_app_uuid}:#{signature}#{AUTH_HEADER_DELIMITER}",
|
51
|
+
'MCC-Time' => attributes[:time]
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def signature_v1(string_to_sign)
|
56
|
+
assert_private_key(UNABLE_TO_SIGN_ERR)
|
57
|
+
hashed_string_to_sign = Digest::SHA512.hexdigest(string_to_sign)
|
58
|
+
Base64.encode64(private_key.private_encrypt(hashed_string_to_sign)).delete("\n")
|
59
|
+
end
|
60
|
+
|
61
|
+
def signature_v2(string_to_sign)
|
62
|
+
assert_private_key(UNABLE_TO_SIGN_ERR)
|
63
|
+
Base64.encode64(private_key.sign(SIGNING_DIGEST, string_to_sign)).delete("\n")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|