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.
@@ -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