class Ey::Hmac::Adapter

This class is responsible for forming the canonical string to used to sign requests @abstract override methods {#method}, {#path}, {#body}, {#content_type} and {#content_digest}

Constants

AUTHORIZATION_REGEXP

Attributes

accept_digests[R]
authorization_header[R]
options[R]
request[R]
service[R]
sign_with[R]

Public Class Methods

new(request, options={}) click to toggle source

@param [Object] request signer-specific request implementation @option options [Integer] :version signature version @option options [Integer] :ttl (nil) duration during which HMAC is valid after signed date @option options [String] :authorization_header ('Authorization') Authorization header key. @option options [String] :server ('EyHmac') service name prefixed to {#authorization}. set to {#service} @option options [Symbol] :sign_with (:sha_256) outgoing signature digest algorithm. See {OpenSSL::Digest#new} @option options [Array] :accepted_digests ([:sha_256]) accepted incoming signature digest algorithm. See {OpenSSL::Digest#new}

# File lib/ey-hmac/adapter.rb, line 18
def initialize(request, options={})
  @request, @options = request, options

  @ttl                  = options[:ttl]
  @authorization_header = options[:authorization_header] || 'Authorization'
  @service              = options[:service] || 'EyHmac'
  @sign_with            = options[:sign_with] || :sha256
  @accept_digests       = Array(options[:accept_digests] || :sha256)
end

Public Instance Methods

authenticate!(&block)
Alias for: authenticated!
authenticated!(&block) click to toggle source

@see Ey::Hmac#authenticate!

# File lib/ey-hmac/adapter.rb, line 116
def authenticated!(&block)
  key_id, signature_value = check_signature!
  key_secret = block.call(key_id)

  unless key_secret
    raise Ey::Hmac::MissingSecret,
      "Failed to find secret matching #{key_id.inspect}"
  end

  check_ttl!

  calculated_signatures = accept_digests.map { |ad| signature(key_secret, ad) }
  matching_signature = calculated_signatures.any? { |cs| secure_compare(signature_value, cs) }

  unless matching_signature
    raise Ey::Hmac::SignatureMismatch,
      "Calculated signature #{signature_value} does not match #{calculated_signatures.inspect} using #{canonicalize.inspect}"
  end

  true
end
Also aliased as: authenticate!
authenticated?(options={}, &block) click to toggle source

Check {#authorization_signature} against calculated {#signature} @yieldparam key_id [String] public HMAC key @return [Boolean] true if block yields matching private key and signature matches, else false @see authenticated!

# File lib/ey-hmac/adapter.rb, line 109
def authenticated?(options={}, &block)
  authenticated!(&block)
rescue Ey::Hmac::Error
  false
end
authorization(key_id, key_secret) click to toggle source

@param [String] key_id public HMAC key @param [String] key_secret private HMAC key @return [String] HMAC header value of {#request}

# File lib/ey-hmac/adapter.rb, line 48
def authorization(key_id, key_secret)
  "#{service} #{key_id}:#{signature(key_secret, sign_with)}"
end
authorization_signature() click to toggle source

@abstract used when verifying a signed request @return [String] value of the {#authorization_header}

# File lib/ey-hmac/adapter.rb, line 93
def authorization_signature
  raise NotImplementedError
end
body() click to toggle source

@abstract @return [String] request body. @return [NilClass] if there is no body or the body is empty

# File lib/ey-hmac/adapter.rb, line 74
def body
  raise NotImplementedError
end
canonicalize() click to toggle source

In order for the server to correctly authorize the request, the client and server MUST AGREE on this format

default canonical string formation is '{#method}\n{#content_type}\n{#content_digest}\n{#date}\n{#path}' @return [String] canonical string used to form the {#signature}

# File lib/ey-hmac/adapter.rb, line 32
def canonicalize
  [method, content_type, content_digest, date, path].join("\n")
end
content_digest() click to toggle source

@abstract Digest of body. Default is MD5. @return [String] digest of body

# File lib/ey-hmac/adapter.rb, line 67
def content_digest
  raise NotImplementedError
end
content_type() click to toggle source

@abstract @return [String] value of the Content-Type header in {#request}

# File lib/ey-hmac/adapter.rb, line 80
def content_type
  raise NotImplementedError
end
date() click to toggle source

@abstract @return [String] value of the Date header in {#request}. @see Time#http_date

# File lib/ey-hmac/adapter.rb, line 87
def date
  raise NotImplementedError
end
method() click to toggle source

@abstract @return [String] upcased request verb. i.e. 'GET'

# File lib/ey-hmac/adapter.rb, line 54
def method
  raise NotImplementedError
end
path() click to toggle source

@abstract @return [String] request path. i.e. '/blogs/1'

# File lib/ey-hmac/adapter.rb, line 60
def path
  raise NotImplementedError
end
sign!(key_id, key_secret) click to toggle source

@abstract Add {#signature} header to request. Typically this is 'Authorization' or 'WWW-Authorization' @return [String] calculated {#authorization} @see Ey::Hmac#sign!

# File lib/ey-hmac/adapter.rb, line 101
def sign!(key_id, key_secret)
  raise NotImplementedError
end
signature(key_secret, digest = self.sign_with) click to toggle source

@param [String] key_secret private HMAC key @param [String] signature digest hash function. Defaults to sign_with @return [String] HMAC signature of {#request}

# File lib/ey-hmac/adapter.rb, line 39
def signature(key_secret, digest = self.sign_with)
  Base64.strict_encode64(
    OpenSSL::HMAC.digest(
      OpenSSL::Digest.new(digest.to_s), key_secret, canonicalize)).strip
end

Protected Instance Methods

check_signature!() click to toggle source
# File lib/ey-hmac/adapter.rb, line 165
def check_signature!
  authorization_match = AUTHORIZATION_REGEXP.match(authorization_signature)

  unless authorization_match
    raise Ey::Hmac::MissingAuthorization,
      "Failed to parse authorization_signature #{authorization_signature}"
  end

  [authorization_match[1], authorization_match[2]]
end
check_ttl!() click to toggle source
# File lib/ey-hmac/adapter.rb, line 153
def check_ttl!
  if @ttl && date
    expiry       = Time.parse(date).to_i + @ttl
    current_time = Time.now.to_i

    unless expiry > current_time
      raise Ey::Hmac::ExpiredHmac,
        "Signature has expired passed #{expiry}. Current time is #{current_time}"
    end
  end
end
secure_compare(a, b) click to toggle source

Constant time string comparison. pulled from github.com/rack/rack/blob/master/lib/rack/utils.rb#L399

# File lib/ey-hmac/adapter.rb, line 143
def secure_compare(a, b)
  return false unless a.bytesize == b.bytesize

  l = a.unpack("C*")

  r, i = 0, -1
  b.each_byte { |v| r |= v ^ l[i+=1] }
  r == 0
end