Skip to content

Commit

Permalink
Write a middleware for Faraday
Browse files Browse the repository at this point in the history
Unlike other HTTP client libraries, Faraday does not expose request objets too
easily. Instead, it provides a framework for configuring connections.
Authorization in Faraday is meant to be handled through the use of middlewares.

By convention, Faraday middlewares are defined under the Faraday namespace,
even when they’re not official. Like other middlewares, requiring
`faraday/api_auth` registers the middleware into Faraday with a name. Since
that side effect depends on the presence of Faraday, it cannot be part of
ApiAuth directly.

The previously-existing Faraday integration still exists for whoever managed to
use it, though I suggest deprecating it.
  • Loading branch information
fmang authored and fwininger committed Aug 3, 2022
1 parent c734a88 commit 1322051
Show file tree
Hide file tree
Showing 9 changed files with 370 additions and 3 deletions.
7 changes: 4 additions & 3 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2022-03-17 08:35:06 UTC using RuboCop version 1.26.0.
# on 2022-08-03 07:19:11 UTC using RuboCop version 1.32.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
Expand Down Expand Up @@ -36,15 +36,16 @@ Lint/Void:
# Offense count: 2
# Configuration parameters: IgnoredMethods.
Metrics/CyclomaticComplexity:
Max: 15
Max: 16

# Offense count: 10
# Offense count: 11
Naming/AccessorMethodName:
Exclude:
- 'lib/api_auth/railtie.rb'
- 'lib/api_auth/request_drivers/action_controller.rb'
- 'lib/api_auth/request_drivers/curb.rb'
- 'lib/api_auth/request_drivers/faraday.rb'
- 'lib/api_auth/request_drivers/faraday_env.rb'
- 'lib/api_auth/request_drivers/grape_request.rb'
- 'lib/api_auth/request_drivers/http.rb'
- 'lib/api_auth/request_drivers/httpi.rb'
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,19 @@ Simply add this configuration to your Flexirest initializer in your app and it w
Flexirest::Base.api_auth_credentials(@access_id, @secret_key)
```

### Faraday

ApiAuth provides a middleware for adding authentication to a Faraday connection:

```ruby
require 'faraday/api_auth'
Faraday.new do |f|
f.request :api_auth, @access_id, @secret_key
end
```

The order of middlewares is important. You should make sure api_auth is last.

## Server

ApiAuth provides some built in methods to help you generate API keys for your
Expand Down
1 change: 1 addition & 0 deletions lib/api_auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
require 'api_auth/request_drivers/rack'
require 'api_auth/request_drivers/httpi'
require 'api_auth/request_drivers/faraday'
require 'api_auth/request_drivers/faraday_env'
require 'api_auth/request_drivers/http'

require 'api_auth/headers'
Expand Down
2 changes: 2 additions & 0 deletions lib/api_auth/headers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ def initialize_request_driver(request, authorize_md5: false)
HttpiRequest.new(request)
when /Faraday::Request/
FaradayRequest.new(request)
when /Faraday::Env/
FaradayEnv.new(request)
when /HTTP::Request/
HttpRequest.new(request)
end
Expand Down
102 changes: 102 additions & 0 deletions lib/api_auth/request_drivers/faraday_env.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
module ApiAuth
module RequestDrivers # :nodoc:
# Internally, Faraday uses the class Faraday::Env to represent requests. The class is not meant
# to be directly exposed to users, but this is what Faraday middlewares work with. See
# <https://lostisland.github.io/faraday/middleware/>.
class FaradayEnv
include ApiAuth::Helpers

def initialize(env)
@env = env
end

def set_auth_header(header)
@env.request_headers['Authorization'] = header
@env
end

def calculated_hash
sha256_base64digest(body)
end

def populate_content_hash
return unless %w[POST PUT PATCH].include?(http_method)

@env.request_headers['X-Authorization-Content-SHA256'] = calculated_hash
end

def content_hash_mismatch?
if %w[POST PUT PATCH].include?(http_method)
calculated_hash != content_hash
else
false
end
end

def http_method
@env.method.to_s.upcase
end

def content_type
type = find_header(%w[CONTENT-TYPE CONTENT_TYPE HTTP_CONTENT_TYPE])

# When sending a body-less POST request, the Content-Type is set at the last minute by the
# Net::HTTP adapter, which states in the documentation for Net::HTTP#post:
#
# > You should set Content-Type: header field for POST. If no Content-Type: field given,
# > this method uses “application/x-www-form-urlencoded” by default.
#
# The same applies to PATCH and PUT. Hopefully the other HTTP adapters behave similarly.
#
type ||= 'application/x-www-form-urlencoded' if %w[POST PATCH PUT].include?(http_method)

type
end

def content_hash
find_header(%w[X-AUTHORIZATION-CONTENT-SHA256])
end

def original_uri
find_header(%w[X-ORIGINAL-URI X_ORIGINAL_URI HTTP_X_ORIGINAL_URI])
end

def request_uri
@env.url.request_uri
end

def set_date
@env.request_headers['Date'] = Time.now.utc.httpdate
end

def timestamp
find_header(%w[DATE HTTP_DATE])
end

def authorization_header
find_header(%w[Authorization AUTHORIZATION HTTP_AUTHORIZATION])
end

def body
body_source = @env.request_body
if body_source.respond_to?(:read)
result = body_source.read
body_source.rewind
result
else
body_source.to_s
end
end

def fetch_headers
capitalize_keys @env.request_headers
end

private

def find_header(keys)
keys.map { |key| @env.request_headers[key] }.compact.first
end
end
end
end
8 changes: 8 additions & 0 deletions lib/faraday/api_auth.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require_relative 'api_auth/middleware'

module Faraday
# Integrate ApiAuth into Faraday.
module ApiAuth
Faraday::Request.register_middleware(api_auth: Middleware)
end
end
35 changes: 35 additions & 0 deletions lib/faraday/api_auth/middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require 'api_auth'

module Faraday
module ApiAuth
# Request middleware for Faraday. It takes the same arguments as ApiAuth.sign!.
#
# You will usually need to include it after the other middlewares since ApiAuth needs to hash
# the final request.
#
# Usage:
#
# ```ruby
# require 'faraday/api_auth'
#
# conn = Faraday.new do |f|
# f.request :api_auth, access_id, secret_key
# # Alternatively:
# # f.use Faraday::ApiAuth::Middleware, access_id, secret_key
# end
# ```
#
class Middleware < Faraday::Middleware
def initialize(app, access_id, secret_key, options = {})
super(app)
@access_id = access_id
@secret_key = secret_key
@options = options
end

def on_request(env)
::ApiAuth.sign!(env, @access_id, @secret_key, @options)
end
end
end
end
17 changes: 17 additions & 0 deletions spec/faraday_middleware_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require 'spec_helper'
require 'faraday/api_auth'

describe Faraday::ApiAuth::Middleware do
it 'adds the Authorization headers' do
conn = Faraday.new('http://localhost/') do |f|
f.request :api_auth, 'foo', 'secret', digest: 'sha256'
f.adapter :test do |stub|
stub.get('http://localhost/test') do |env|
[200, {}, env.request_headers['Authorization']]
end
end
end
response = conn.get('test', nil, { 'Date' => 'Tue, 02 Aug 2022 09:29:24 GMT' })
expect(response.body).to eq 'APIAuth-HMAC-SHA256 foo:Tn/lIZ9kphcO32DwG4wFHenqBt37miDEIkA5ykLgGiQ='
end
end
Loading

0 comments on commit 1322051

Please sign in to comment.