class Pusher::Signature::Request

Constants

ISO8601

www.w3.org/TR/NOTE-datetime

Attributes

path[RW]
query_hash[RW]

Public Class Methods

new(method, path, query) click to toggle source
# 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

auth_hash() click to toggle source

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(timestamp_grace = 600) { |key| ... } click to toggle source

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_async(timestamp_grace = 600) { |key| ... } click to toggle source

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_by_token(token, timestamp_grace = 600) click to toggle source

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
authenticate_by_token!(token, timestamp_grace = 600) click to toggle source

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(token) click to toggle source

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
signed_params() click to toggle source

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

identical?(a, b) click to toggle source

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
parameter_string() click to toggle source
# 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
signature(token) click to toggle source
# 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
string_to_sign() click to toggle source
# File lib/pusher/signature.rb, line 177
def string_to_sign
  [@method, @path, parameter_string].join("\n")
end
validate_signature!(token) click to toggle source
# 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
validate_timestamp!(grace) click to toggle source
# 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
validate_version!() click to toggle source
# 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