vendor/signature/lib/signature.rb in scout-5.7.1 vs vendor/signature/lib/signature.rb in scout-5.7.2.pre

- old
+ new

@@ -1,7 +1,9 @@ -require 'hmac-sha2' +require 'openssl' +require 'signature/query_encoder' + module Signature class AuthenticationError < RuntimeError; end class Token attr_reader :key, :secret @@ -16,10 +18,12 @@ end class Request attr_accessor :path, :query_hash + include QueryEncoder + # http://www.w3.org/TR/NOTE-datetime ISO8601 = "%Y-%m-%dT%H:%M:%SZ" def initialize(method, path, query) raise ArgumentError, "Expected string" unless path.kind_of?(String) @@ -32,67 +36,143 @@ k[0..4] == 'auth_' ? auth_hash[k] = v : query_hash[k] = v end @method = method.upcase @path, @query_hash, @auth_hash = path, query_hash, auth_hash + @signed = false end + # Sign the request with the given token, and return the computed + # authentication parameters + # def sign(token) @auth_hash = { :auth_version => "1.0", :auth_key => token.key, :auth_timestamp => Time.now.to_i.to_s } - @auth_hash[:auth_signature] = signature(token) + @signed = true + return @auth_hash end # Authenticates the request with a token # - # Timestamp check: Unless timestamp_grace is set to nil (which will skip - # the timestamp check), an exception will be raised if timestamp is not - # supplied or if the timestamp provided is not within timestamp_grace of - # the real time (defaults to 10 minutes) + # Raises an AuthenticationError if the request is invalid. + # AuthenticationError exception messages are designed to be exposed to API + # consumers, and should help them correct errors generating signatures # - # Signature check: Raises an exception if the signature does not match the - # computed value + # Timestamp: Unless timestamp_grace is set to nil (which allows this check + # to be skipped), AuthenticationError will be raised if the timestamp is + # missing or further than timestamp_grace period away from the real time + # (defaults to 10 minutes) # + # Signature: Raises AuthenticationError if the signature does not match + # the computed HMAC. The error contains a hint for how to sign. + # def authenticate_by_token!(token, timestamp_grace = 600) + # Validate that your code has provided a valid token. This does not + # raise an AuthenticationError since passing tokens with empty secret is + # a code error which should be fixed, not reported to the API's consumer + if token.secret.nil? || token.secret.empty? + raise "Provided token is missing secret" + end + validate_version! validate_timestamp!(timestamp_grace) validate_signature!(token) true end + # Authenticate the request with a token, but rather than raising an + # exception if the request is invalid, simply returns false + # def authenticate_by_token(token, timestamp_grace = 600) authenticate_by_token!(token, timestamp_grace) rescue AuthenticationError false end - def authenticate(timestamp_grace = 600, &block) + # Authenticate a request + # + # Takes a block which will be called with the auth_key from the request, + # and which should return a Signature::Token (or nil if no token can be + # found for the key) + # + # Raises errors in the same way as authenticate_by_token! + # + def authenticate(timestamp_grace = 600) + raise ArgumentError, "Block required" unless block_given? key = @auth_hash['auth_key'] - raise AuthenticationError, "Authentication key required" unless key + raise AuthenticationError, "Missing parameter: auth_key" unless key token = yield key - unless token && token.secret - raise AuthenticationError, "Invalid authentication key" + unless token + raise AuthenticationError, "Unknown auth_key" end authenticate_by_token!(token, timestamp_grace) return token end + # Authenticate a request asynchronously + # + # This method is useful it you're running a server inside eventmachine and + # need to lookup the token asynchronously. + # + # The block is passed an auth key and a deferrable which should succeed + # with the token, or fail if the token cannot be found + # + # This method returns a deferrable which succeeds with the valid token, or + # fails with an AuthenticationError which can be used to pass the error + # back to the user + # + def authenticate_async(timestamp_grace = 600) + raise ArgumentError, "Block required" unless block_given? + df = EM::DefaultDeferrable.new + + key = @auth_hash['auth_key'] + + unless key + df.fail(AuthenticationError.new("Missing parameter: auth_key")) + return + end + + token_df = yield key + token_df.callback { |token| + begin + authenticate_by_token!(token, timestamp_grace) + df.succeed(token) + rescue AuthenticationError => e + df.fail(e) + end + } + token_df.errback { + df.fail(AuthenticationError.new("Unknown auth_key")) + } + ensure + return df + end + + # Expose the authentication parameters for a signed request + # def auth_hash - raise "Request not signed" unless @auth_hash && @auth_hash[:auth_signature] + raise "Request not signed" unless @signed @auth_hash end + # Query parameters merged with the computed authentication parameters + # + def signed_params + @query_hash.merge(auth_hash) + end + private def signature(token) - HMAC::SHA256.hexdigest(token.secret, string_to_sign) + digest = OpenSSL::Digest::SHA256.new + OpenSSL::HMAC.hexdigest(digest, token.secret, string_to_sign) end def string_to_sign [@method, @path, parameter_string].join("\n") end @@ -104,10 +184,12 @@ hash = {}; param_hash.each { |k,v| hash[k.to_s.downcase] = v } # Exclude signature from signature generation! hash.delete("auth_signature") - hash.keys.sort.map { |k| "#{k}=#{hash[k]}" }.join("&") + hash.sort.map do |k, v| + QueryEncoder.encode_param_without_escaping(k, v) + end.join('&') end def validate_version! version = @auth_hash["auth_version"] raise AuthenticationError, "Version required" unless version