ApiSigv2

Simple HMAC-SHA1 authentication via headers. Impressed by AWS Requests with Signature Version 4

This gem will generate signature for the client requests and verify that signature on the server side

Installation

Add this line to your application's Gemfile:

gem 'api_sigv2'

Usage

The usage is pretty simple. To sign a request use ApiSigv2::Signer and for validation use ApiSigv2::Validator.

Create signature

Sign a request with 'authorization' header. You can change header name, see Configuration section.

api_access_key = 'access_key'
api_secret_key = 'secret_key'

request = {
  http_method: 'POST',
  url: 'https://example.com/posts',
  headers: {
    'User-Agent' => 'Test agent'
  },
  body: 'body'
}

# Sign your request
signature = ApiSigv2::Signer.new(api_access_key, api_secret_key).sign_request(request)

# Now apply signed headers to your real request
signature.headers

# signature.headers looks like:
{
  "host"=>"example.com",
  "x-datetime"=>"2020-01-02T10:24:59.837+0000",
  "authorization"=>"API-HMAC-SHA256 Credential=access_key/20200102/web/api_request, SignedHeaders=host;user-agent;x-datetime, Signature=032fc0b7defd66d86ef43ced8e6c3ee351ede21deca6bf1f89b9145f7a9105c1"
}

Validate signature

Validate the request on the client-side. Note, that access_key can be extracted from the request.

# the request to validate
request = {
  :http_method=>"POST",
  :url=>"https://example.com/posts",
  :headers=>{
    "User-Agent"=>"Test agent",
    "host"=>"example.com",
    "x-datetime"=>"2020-01-02T10:24:59.837+0000",
    "authorization"=>"API-HMAC-SHA256 Credential=access_key/20200102/web/api_request, SignedHeaders=host;user-agent;x-datetime, Signature=032fc0b7defd66d86ef43ced8e6c3ee351ede21deca6bf1f89b9145f7a9105c1"
  },
  :body=>"body"
}

# initialize validator with a request to validate
validator = ApiSigv2::Validator.new(request)

# get access key from request headers (String)
validator.access_key

# validate the request (Boolean)
validator.valid?('your secret key here')

# get only signed headers (Hash)
validator.signed_headers

Configuration

By default, the generated signature will be valid for 5 minutes This could be changed via initializer:

# config/initializers/api_sigv2.rb

ApiSigv2.setup do |config|
  # Time to live, by default 5 minutes
  config.signature_ttl = 5 * 60

  # Datetime format, by default iso8601
  config.datetime_format = '%Y-%m-%dT%H:%M:%S.%L%z'

  # Header name, by default authorization
  config.signature_header = 'authorization'

  # Service name, by default web
  config.service = 'web'
end

Testing

In your rails_helper.rb:

require 'api_sigv2/spec_support/helper'

RSpec.configure do |config|
  config.include ApiSigv2::SpecSupport::Helper, type: :controller
end

This will enable the following methods in controller tests:

  • get_with_signature(client, action_name, params = {})
  • post_with_signature(client, action_name, params = {})
  • put_with_signature(client, action_name, params = {})
  • patch_with_signature(client, action_name, params = {})
  • delete_with_signature(client, action_name, params = {})

client object should respond to #api_key and #api_secret

Example usage:

RSpec.describe Api::V1::OrdersController do
  let(:client) { FactoryBot.create(:client) }
  # or any object, that responds to #api_key and #api_secret
  # let(:client) { OpenStruct.new(api_key: 'some_key', api_secret: 'some_api_secret') }

  it 'should filter orders by state' do
    get_with_signature client, :index, state: :paid

    expect(last_response.status).to eq 200
    expect(last_response.body).to have_node(:orders)
    expect(last_response.body).to have_node(:state).with('paid')
  end

  let(:order_attributes) { FactoryBot.attributes_for(:order) }

  it 'should create new order' do
    post_with_signature client, :create, order: order_attributes
  end
end

For nested resources path can be specified explicitly using path parameter:

# path: /api/v1/orders/:order_id/comments

RSpec.describe Api::V1::CommentsController do
  let(:client) { OpenStruct.new(api_key: 'some_key', api_secret: 'some_api_secret') }
  let(:order) { FactoryBot.create(:order) }

  it 'should update comment for order' do
    put_with_signature client, :update, path: { order_id: order.id }, comment: { content: 'Some value' }
  end
end

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/api_sigv2.

License

The gem is available as open source under the terms of the MIT License.