module AlexaRequestVerifier

Verify that HTTP requests sent to an Alexa skill are sent from Amazon @since 0.1.0

Constants

REQUEST_THRESHOLD
VERSION

Public Class Methods

valid!(request) click to toggle source

Validate a request object from Sinatra. Raise an error if it is not valid.

@param [Sinatra::Request] request a Sinatra HTTP Request

@raise [AlexaRequestVerifier::InvalidCertificateURIError]

there was a problem validating the certificate URI from your request

@return [nil] will always return nil

# File lib/alexa_request_verifier.rb, line 26
def valid!(request)
  signature_certificate_url = request.env['HTTP_SIGNATURECERTCHAINURL']

  AlexaRequestVerifier::Verifier::CertificateURIVerifier.valid!(signature_certificate_url)

  raw_body = request.body.read
  request.body.rewind

  check_that_request_is_timely(raw_body)

  check_that_request_is_valid(signature_certificate_url, request, raw_body)

  true
end
valid?(request) click to toggle source

Validate a request object from Sinatra. Return a boolean.

@param [Sinatra::Request] request a Sinatra HTTP Request @return [Boolean] is the request valid?

# File lib/alexa_request_verifier.rb, line 46
def valid?(request)
  begin
    valid!(request)
  rescue AlexaRequestVerifier::BaseError => e
    puts e

    return false
  end

  true
end

Private Class Methods

check_that_request_is_timely(raw_body) click to toggle source

Prevent replays of requests by checking that they are timely.

@param [String] raw_body the raw body of our https request @raise [AlexaRequestVerifier::InvalidRequestError] raised when the timestamp is not timely, or is not set

# File lib/alexa_request_verifier.rb, line 64
def check_that_request_is_timely(raw_body)
  request_json = JSON.parse(raw_body)

  raise AlexaRequestVerifier::InvalidRequestError, 'Timestamp field not present in request' if request_json.fetch('request', {}).fetch('timestamp', nil).nil?

  raise AlexaRequestVerifier::InvalidRequestError, 'Request is from more than 150 seconds ago' unless Time.parse(request_json['request']['timestamp'].to_s) >= (Time.now - REQUEST_THRESHOLD)
end
check_that_request_is_valid(signature_certificate_url, request, raw_body) click to toggle source

Check that our request is valid.

@param [String] signature_certificate_url the url for our signing certificate @param [Sinatra::Request] request the request object @param [String] raw_body the raw body of our https request

# File lib/alexa_request_verifier.rb, line 77
def check_that_request_is_valid(signature_certificate_url, request, raw_body)
  certificate = AlexaRequestVerifier::CertificateStore.fetch(signature_certificate_url)

  begin
    AlexaRequestVerifier::Verifier::CertificateVerifier.valid!(certificate)

    check_that_request_was_signed(certificate.public_key, request, raw_body)
  rescue AlexaRequestVerifier::InvalidCertificateError, AlexaRequestVerifier::InvalidRequestError => error
    # We don't want to cache a certificate that fails our checks as it could lock us out of valid requests for the cache length
    AlexaRequestVerifier::CertificateStore.delete(signature_certificate_url)

    raise error
  end
end
check_that_request_was_signed(certificate_public_key, request, raw_body) click to toggle source

Check that our request was signed by a given public key.

@param [OpenSSL::PKey::PKey] certificate_public_key the public key we are checking @param [Sinatra::Request] request the request object we are checking @param [String] raw_body the raw body of our https request @raise [AlexaRequestVerifier::InvalidRequestError] raised if our signature does not match the certificate provided

# File lib/alexa_request_verifier.rb, line 98
def check_that_request_was_signed(certificate_public_key, request, raw_body)
  signed_by_certificate = certificate_public_key.verify(
    OpenSSL::Digest::SHA1.new,
    Base64.decode64(request.env['HTTP_SIGNATURE']),
    raw_body
  )

  raise AlexaRequestVerifier::InvalidRequestError, 'Signature does not match certificate provided' unless signed_by_certificate
end