class Pusher::Signature::Request
Constants
- ISO8601
Attributes
Public Class Methods
# File lib/pusher/signature.rb, line 29 def initialize(method, path, query) raise ArgumentError, "Expected string" unless path.kind_of?(String) raise ArgumentError, "Expected hash" unless query.kind_of?(Hash) query_hash = {} auth_hash = {} query.each do |key, v| k = key.to_s.downcase 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
Public Instance Methods
Expose the authentication parameters for a signed request
# File lib/pusher/signature.rb, line 159 def auth_hash raise "Request not signed" unless @signed @auth_hash end
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
!
# File lib/pusher/signature.rb, line 106 def authenticate(timestamp_grace = 600) raise ArgumentError, "Block required" unless block_given? key = @auth_hash['auth_key'] raise AuthenticationError, "Missing parameter: auth_key" unless key token = yield 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
# File lib/pusher/signature.rb, line 130 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
Authenticate the request with a token, but rather than raising an exception if the request is invalid, simply returns false
# File lib/pusher/signature.rb, line 92 def authenticate_by_token(token, timestamp_grace = 600) authenticate_by_token!(token, timestamp_grace) rescue AuthenticationError false end
Authenticates the request with a token
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
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.
# File lib/pusher/signature.rb, line 75 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
Sign the request with the given token, and return the computed authentication parameters
# File lib/pusher/signature.rb, line 48 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
Query parameters merged with the computed authentication parameters
# File lib/pusher/signature.rb, line 166 def signed_params @query_hash.merge(auth_hash) end
Private Instance Methods
Constant time string comparison
# File lib/pusher/signature.rb, line 226 def identical?(a, b) return true if a.nil? && b.nil? return false if a.nil? || b.nil? return false unless a.bytesize == b.bytesize a.bytes.zip(b.bytes).reduce(0) { |memo, (a, b)| memo += a ^ b } == 0 end
# File lib/pusher/signature.rb, line 181 def parameter_string param_hash = @query_hash.merge(@auth_hash || {}) # Convert keys to lowercase strings hash = {}; param_hash.each { |k,v| hash[k.to_s.downcase] = v } # Exclude signature from signature generation! hash.delete("auth_signature") hash.sort.map do |k, v| QueryEncoder.encode_param_without_escaping(k, v) end.join('&') end
# File lib/pusher/signature.rb, line 172 def signature(token) digest = OpenSSL::Digest::SHA256.new OpenSSL::HMAC.hexdigest(digest, token.secret, string_to_sign) end
# File lib/pusher/signature.rb, line 177 def string_to_sign [@method, @path, parameter_string].join("\n") end
# File lib/pusher/signature.rb, line 216 def validate_signature!(token) unless identical? @auth_hash["auth_signature"], signature(token) raise AuthenticationError, "Invalid signature: you should have "\ "sent HmacSHA256Hex(#{string_to_sign.inspect}, your_secret_key)"\ ", but you sent #{@auth_hash["auth_signature"].inspect}" end return true end
# File lib/pusher/signature.rb, line 201 def validate_timestamp!(grace) return true if grace.nil? timestamp = @auth_hash["auth_timestamp"] error = (timestamp.to_i - Time.now.to_i).abs raise AuthenticationError, "Timestamp required" unless timestamp if error >= grace raise AuthenticationError, "Timestamp expired: Given timestamp "\ "(#{Time.at(timestamp.to_i).utc.strftime(ISO8601)}) "\ "not within #{grace}s of server time "\ "(#{Time.now.utc.strftime(ISO8601)})" end return true end
# File lib/pusher/signature.rb, line 195 def validate_version! version = @auth_hash["auth_version"] raise AuthenticationError, "Version required" unless version raise AuthenticationError, "Version not supported" unless version == '1.0' end