module ApiAuth

The gem will sign your requests on the client side and authenticate that signature on the server side. If your server resources are implemented as a Rails ActiveResource, it will integrate with that. It will even generate the secret keys necessary for your clients to sign their requests.

Constants

AUTH_HEADER_PATTERN

Public Class Methods

access_id(request) click to toggle source

Returns the access id from the request’s authorization header

# File lib/api_auth/base.rb, line 54
def access_id(request)
  headers = Headers.new(request)
  if match_data = parse_auth_header(headers.authorization_header)
    return match_data[2]
  end

  nil
end
authentic?(request, secret_key, options = {}) click to toggle source

Determines if the request is authentic given the request and the client’s secret key. Returns true if the request is authentic and false otherwise.

# File lib/api_auth/base.rb, line 32
def authentic?(request, secret_key, options = {})
  return false if secret_key.nil?

  options = { override_http_method: nil }.merge(options)

  headers = Headers.new(request)

  # 900 seconds is 15 minutes
  clock_skew = options.fetch(:clock_skew, 900)

  if headers.content_hash_mismatch?
    false
  elsif !signatures_match?(headers, secret_key, options)
    false
  elsif !request_within_time_window?(headers, clock_skew)
    false
  else
    true
  end
end
generate_secret_key() click to toggle source

Generates a Base64 encoded, randomized secret key

Store this key along with the access key that will be used for authenticating the client

# File lib/api_auth/base.rb, line 67
def generate_secret_key
  random_bytes = OpenSSL::Random.random_bytes(512)
  b64_encode(Digest::SHA2.new(512).digest(random_bytes))
end
sign!(request, access_id, secret_key, options = {}) click to toggle source

Signs an HTTP request using the client’s access id and secret key. Returns the HTTP request object with the modified headers.

request: The request can be a Net::HTTP, ActionDispatch::Request, Curb (Curl::Easy), RestClient object or Faraday::Request.

access_id: The public unique identifier for the client

secret_key: assigned secret key that is known to both parties

# File lib/api_auth/base.rb, line 22
def sign!(request, access_id, secret_key, options = {})
  options = { override_http_method: nil, digest: 'sha1' }.merge(options)
  headers = Headers.new(request)
  headers.calculate_hash
  headers.set_date
  headers.sign_header auth_header(headers, access_id, secret_key, options)
end

Private Class Methods

auth_header(headers, access_id, secret_key, options) click to toggle source
# File lib/api_auth/base.rb, line 113
def auth_header(headers, access_id, secret_key, options)
  hmac_string = "-HMAC-#{options[:digest].upcase}" unless options[:digest] == 'sha1'
  "APIAuth#{hmac_string} #{access_id}:#{hmac_signature(headers, secret_key, options)}"
end
hmac_signature(headers, secret_key, options) click to toggle source
# File lib/api_auth/base.rb, line 107
def hmac_signature(headers, secret_key, options)
  canonical_string = headers.canonical_string(options[:override_http_method], options[:headers_to_sign])
  digest = OpenSSL::Digest.new(options[:digest])
  b64_encode(OpenSSL::HMAC.digest(digest, secret_key, canonical_string))
end
parse_auth_header(auth_header) click to toggle source
# File lib/api_auth/base.rb, line 118
def parse_auth_header(auth_header)
  AUTH_HEADER_PATTERN.match(auth_header)
end
request_within_time_window?(headers, clock_skew) click to toggle source
# File lib/api_auth/base.rb, line 76
def request_within_time_window?(headers, clock_skew)
  Time.httpdate(headers.timestamp).utc > (Time.now.utc - clock_skew) &&
    Time.httpdate(headers.timestamp).utc < (Time.now.utc + clock_skew)
rescue ArgumentError
  false
end
secure_equals?(m1, m2, key) click to toggle source
# File lib/api_auth/base.rb, line 98
def secure_equals?(m1, m2, key)
  sha1_hmac(key, m1) == sha1_hmac(key, m2)
end
sha1_hmac(key, message) click to toggle source
# File lib/api_auth/base.rb, line 102
def sha1_hmac(key, message)
  digest = OpenSSL::Digest.new('sha1')
  OpenSSL::HMAC.digest(digest, key, message)
end
signatures_match?(headers, secret_key, options) click to toggle source
# File lib/api_auth/base.rb, line 83
def signatures_match?(headers, secret_key, options)
  match_data = parse_auth_header(headers.authorization_header)
  return false unless match_data

  digest = match_data[1].nil? ? 'SHA1' : match_data[1].upcase
  raise InvalidRequestDigest if !options[:digest].nil? && !options[:digest].casecmp(digest).zero?

  options = { digest: digest }.merge(options)

  header_sig = match_data[3]
  calculated_sig = hmac_signature(headers, secret_key, options)

  secure_equals?(header_sig, calculated_sig, secret_key)
end