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