Skip to content

Commit 1322051

Browse files
fmangfwininger
authored andcommitted
Write a middleware for Faraday
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.
1 parent c734a88 commit 1322051

File tree

9 files changed

+370
-3
lines changed

9 files changed

+370
-3
lines changed

.rubocop_todo.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config`
3-
# on 2022-03-17 08:35:06 UTC using RuboCop version 1.26.0.
3+
# on 2022-08-03 07:19:11 UTC using RuboCop version 1.32.0.
44
# The point is for the user to remove these configuration records
55
# one by one as the offenses are removed from the code base.
66
# Note that changes in the inspected code, or installation of new
@@ -36,15 +36,16 @@ Lint/Void:
3636
# Offense count: 2
3737
# Configuration parameters: IgnoredMethods.
3838
Metrics/CyclomaticComplexity:
39-
Max: 15
39+
Max: 16
4040

41-
# Offense count: 10
41+
# Offense count: 11
4242
Naming/AccessorMethodName:
4343
Exclude:
4444
- 'lib/api_auth/railtie.rb'
4545
- 'lib/api_auth/request_drivers/action_controller.rb'
4646
- 'lib/api_auth/request_drivers/curb.rb'
4747
- 'lib/api_auth/request_drivers/faraday.rb'
48+
- 'lib/api_auth/request_drivers/faraday_env.rb'
4849
- 'lib/api_auth/request_drivers/grape_request.rb'
4950
- 'lib/api_auth/request_drivers/http.rb'
5051
- 'lib/api_auth/request_drivers/httpi.rb'

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,19 @@ Simply add this configuration to your Flexirest initializer in your app and it w
182182
Flexirest::Base.api_auth_credentials(@access_id, @secret_key)
183183
```
184184

185+
### Faraday
186+
187+
ApiAuth provides a middleware for adding authentication to a Faraday connection:
188+
189+
```ruby
190+
require 'faraday/api_auth'
191+
Faraday.new do |f|
192+
f.request :api_auth, @access_id, @secret_key
193+
end
194+
```
195+
196+
The order of middlewares is important. You should make sure api_auth is last.
197+
185198
## Server
186199

187200
ApiAuth provides some built in methods to help you generate API keys for your

lib/api_auth.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
require 'api_auth/request_drivers/rack'
1515
require 'api_auth/request_drivers/httpi'
1616
require 'api_auth/request_drivers/faraday'
17+
require 'api_auth/request_drivers/faraday_env'
1718
require 'api_auth/request_drivers/http'
1819

1920
require 'api_auth/headers'

lib/api_auth/headers.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ def initialize_request_driver(request, authorize_md5: false)
3636
HttpiRequest.new(request)
3737
when /Faraday::Request/
3838
FaradayRequest.new(request)
39+
when /Faraday::Env/
40+
FaradayEnv.new(request)
3941
when /HTTP::Request/
4042
HttpRequest.new(request)
4143
end
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
module ApiAuth
2+
module RequestDrivers # :nodoc:
3+
# Internally, Faraday uses the class Faraday::Env to represent requests. The class is not meant
4+
# to be directly exposed to users, but this is what Faraday middlewares work with. See
5+
# <https://lostisland.github.io/faraday/middleware/>.
6+
class FaradayEnv
7+
include ApiAuth::Helpers
8+
9+
def initialize(env)
10+
@env = env
11+
end
12+
13+
def set_auth_header(header)
14+
@env.request_headers['Authorization'] = header
15+
@env
16+
end
17+
18+
def calculated_hash
19+
sha256_base64digest(body)
20+
end
21+
22+
def populate_content_hash
23+
return unless %w[POST PUT PATCH].include?(http_method)
24+
25+
@env.request_headers['X-Authorization-Content-SHA256'] = calculated_hash
26+
end
27+
28+
def content_hash_mismatch?
29+
if %w[POST PUT PATCH].include?(http_method)
30+
calculated_hash != content_hash
31+
else
32+
false
33+
end
34+
end
35+
36+
def http_method
37+
@env.method.to_s.upcase
38+
end
39+
40+
def content_type
41+
type = find_header(%w[CONTENT-TYPE CONTENT_TYPE HTTP_CONTENT_TYPE])
42+
43+
# When sending a body-less POST request, the Content-Type is set at the last minute by the
44+
# Net::HTTP adapter, which states in the documentation for Net::HTTP#post:
45+
#
46+
# > You should set Content-Type: header field for POST. If no Content-Type: field given,
47+
# > this method uses “application/x-www-form-urlencoded” by default.
48+
#
49+
# The same applies to PATCH and PUT. Hopefully the other HTTP adapters behave similarly.
50+
#
51+
type ||= 'application/x-www-form-urlencoded' if %w[POST PATCH PUT].include?(http_method)
52+
53+
type
54+
end
55+
56+
def content_hash
57+
find_header(%w[X-AUTHORIZATION-CONTENT-SHA256])
58+
end
59+
60+
def original_uri
61+
find_header(%w[X-ORIGINAL-URI X_ORIGINAL_URI HTTP_X_ORIGINAL_URI])
62+
end
63+
64+
def request_uri
65+
@env.url.request_uri
66+
end
67+
68+
def set_date
69+
@env.request_headers['Date'] = Time.now.utc.httpdate
70+
end
71+
72+
def timestamp
73+
find_header(%w[DATE HTTP_DATE])
74+
end
75+
76+
def authorization_header
77+
find_header(%w[Authorization AUTHORIZATION HTTP_AUTHORIZATION])
78+
end
79+
80+
def body
81+
body_source = @env.request_body
82+
if body_source.respond_to?(:read)
83+
result = body_source.read
84+
body_source.rewind
85+
result
86+
else
87+
body_source.to_s
88+
end
89+
end
90+
91+
def fetch_headers
92+
capitalize_keys @env.request_headers
93+
end
94+
95+
private
96+
97+
def find_header(keys)
98+
keys.map { |key| @env.request_headers[key] }.compact.first
99+
end
100+
end
101+
end
102+
end

lib/faraday/api_auth.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
require_relative 'api_auth/middleware'
2+
3+
module Faraday
4+
# Integrate ApiAuth into Faraday.
5+
module ApiAuth
6+
Faraday::Request.register_middleware(api_auth: Middleware)
7+
end
8+
end

lib/faraday/api_auth/middleware.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
require 'api_auth'
2+
3+
module Faraday
4+
module ApiAuth
5+
# Request middleware for Faraday. It takes the same arguments as ApiAuth.sign!.
6+
#
7+
# You will usually need to include it after the other middlewares since ApiAuth needs to hash
8+
# the final request.
9+
#
10+
# Usage:
11+
#
12+
# ```ruby
13+
# require 'faraday/api_auth'
14+
#
15+
# conn = Faraday.new do |f|
16+
# f.request :api_auth, access_id, secret_key
17+
# # Alternatively:
18+
# # f.use Faraday::ApiAuth::Middleware, access_id, secret_key
19+
# end
20+
# ```
21+
#
22+
class Middleware < Faraday::Middleware
23+
def initialize(app, access_id, secret_key, options = {})
24+
super(app)
25+
@access_id = access_id
26+
@secret_key = secret_key
27+
@options = options
28+
end
29+
30+
def on_request(env)
31+
::ApiAuth.sign!(env, @access_id, @secret_key, @options)
32+
end
33+
end
34+
end
35+
end

spec/faraday_middleware_spec.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
require 'spec_helper'
2+
require 'faraday/api_auth'
3+
4+
describe Faraday::ApiAuth::Middleware do
5+
it 'adds the Authorization headers' do
6+
conn = Faraday.new('http://localhost/') do |f|
7+
f.request :api_auth, 'foo', 'secret', digest: 'sha256'
8+
f.adapter :test do |stub|
9+
stub.get('http://localhost/test') do |env|
10+
[200, {}, env.request_headers['Authorization']]
11+
end
12+
end
13+
end
14+
response = conn.get('test', nil, { 'Date' => 'Tue, 02 Aug 2022 09:29:24 GMT' })
15+
expect(response.body).to eq 'APIAuth-HMAC-SHA256 foo:Tn/lIZ9kphcO32DwG4wFHenqBt37miDEIkA5ykLgGiQ='
16+
end
17+
end

0 commit comments

Comments
 (0)