Skip to content

Commit f7f2b7b

Browse files
authored
Enhance docs (#217)
1 parent d483573 commit f7f2b7b

File tree

1 file changed

+249
-19
lines changed

1 file changed

+249
-19
lines changed

README.md

Lines changed: 249 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,12 @@ minutes in order to avoid replay attacks.
6565
* [HMAC algorithm](https://en.wikipedia.org/wiki/HMAC)
6666
* [RFC 2104 (HMAC)](https://tools.ietf.org/html/rfc2104)
6767

68-
## Requirement
68+
## Requirements
6969

70-
This gem require Ruby >= 2.6 and Rails >= 6.0 if you use rails.
70+
* Ruby >= 3.2 (for version 3.0+)
71+
* Ruby >= 2.6 (for version 2.x)
72+
* Rails >= 7.2 if using Rails (for version 3.0+)
73+
* Rails >= 6.0 if using Rails (for version 2.x)
7174

7275
## Install
7376

@@ -85,17 +88,21 @@ Please note the dash in the name versus the underscore.
8588
ApiAuth supports many popular HTTP clients. Support for other clients can be
8689
added as a request driver.
8790
88-
Here is the current list of supported request objects:
91+
### Supported HTTP Clients
8992
90-
* Net::HTTP
91-
* ActionDispatch::Request
92-
* Curb (Curl::Easy)
93-
* RestClient
94-
* Faraday
95-
* HTTPI
96-
* HTTP
93+
* **Net::HTTP** - Ruby's standard library HTTP client
94+
* **ActionController::Request** / **ActionDispatch::Request** - Rails request objects
95+
* **Curb** (Curl::Easy) - Ruby libcurl bindings
96+
* **RestClient** - Popular REST client for Ruby
97+
* **Faraday** - Modular HTTP client library (with middleware support)
98+
* **HTTPI** - Common interface for Ruby HTTP clients
99+
* **HTTP** (http.rb) - Fast Ruby HTTP client with a chainable API
100+
* **Grape** - REST-like API framework for Ruby (via Rack)
101+
* **Rack::Request** - Generic Rack request objects
97102

98-
### HTTP Client Objects
103+
### Client Examples
104+
105+
#### RestClient
99106

100107
Here's a sample implementation of signing a request created with RestClient.
101108
@@ -160,6 +167,116 @@ to:
160167
Authorization = APIAuth-HMAC-DIGEST_NAME 'client access id':'signature'
161168
```
162169
170+
#### Net::HTTP
171+
172+
For Ruby's standard Net::HTTP library:
173+
174+
```ruby
175+
require 'net/http'
176+
require 'api_auth'
177+
178+
uri = URI('https://api.example.com/resource')
179+
request = Net::HTTP::Post.new(uri.path)
180+
request.content_type = 'application/json'
181+
request.body = '{"key": "value"}'
182+
183+
# Sign the request
184+
signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
185+
186+
# Send the request
187+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
188+
http.request(signed_request)
189+
end
190+
```
191+
192+
#### Curb (Curl::Easy)
193+
194+
For requests using the Curb library:
195+
196+
```ruby
197+
require 'curb'
198+
require 'api_auth'
199+
200+
request = Curl::Easy.new('https://api.example.com/resource')
201+
request.headers['Content-Type'] = 'application/json'
202+
request.post_body = '{"key": "value"}'
203+
204+
# Sign the request (note: specify the HTTP method for Curb)
205+
ApiAuth.sign!(request, @access_id, @secret_key, override_http_method: 'POST')
206+
207+
# Perform the request
208+
request.perform
209+
```
210+
211+
#### HTTP (http.rb)
212+
213+
For the HTTP.rb library:
214+
215+
```ruby
216+
require 'http'
217+
require 'api_auth'
218+
219+
request = HTTP.headers('Content-Type' => 'application/json')
220+
.post('https://api.example.com/resource',
221+
body: '{"key": "value"}')
222+
223+
# Sign the request
224+
signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
225+
226+
# The request is automatically executed when you call response methods
227+
response = signed_request.to_s
228+
```
229+
230+
#### HTTPI
231+
232+
For HTTPI requests:
233+
234+
```ruby
235+
require 'httpi'
236+
require 'api_auth'
237+
238+
request = HTTPI::Request.new('https://api.example.com/resource')
239+
request.headers['Content-Type'] = 'application/json'
240+
request.body = '{"key": "value"}'
241+
242+
# Sign the request
243+
ApiAuth.sign!(request, @access_id, @secret_key, override_http_method: 'POST')
244+
245+
# Perform the request
246+
response = HTTPI.post(request)
247+
```
248+
249+
#### Faraday
250+
251+
ApiAuth provides a middleware for adding authentication to a Faraday connection:
252+
253+
```ruby
254+
require 'faraday'
255+
require 'faraday/api_auth'
256+
257+
# Using middleware (recommended)
258+
connection = Faraday.new(url: 'https://api.example.com') do |faraday|
259+
faraday.request :json
260+
faraday.request :api_auth, @access_id, @secret_key # Add ApiAuth middleware
261+
faraday.response :json
262+
faraday.adapter Faraday.default_adapter
263+
end
264+
265+
# The middleware will automatically sign all requests
266+
response = connection.post('/resource', { key: 'value' })
267+
268+
# Or manually sign a request
269+
request = Faraday::Request.create(:post) do |req|
270+
req.url 'https://api.example.com/resource'
271+
req.headers['Content-Type'] = 'application/json'
272+
req.body = '{"key": "value"}'
273+
end
274+
275+
signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
276+
```
277+
278+
The order of middlewares is important. You should make sure api_auth is added after any middleware that modifies the request body or content-type header.
279+
163280
### ActiveResource Clients
164281

165282
ApiAuth can transparently protect your ActiveResource communications with a
@@ -182,18 +299,67 @@ Simply add this configuration to your Flexirest initializer in your app and it w
182299
Flexirest::Base.api_auth_credentials(@access_id, @secret_key)
183300
```
184301

185-
### Faraday
302+
### Grape API
186303

187-
ApiAuth provides a middleware for adding authentication to a Faraday connection:
304+
For Grape API applications, the request is automatically accessible:
188305

189306
```ruby
190-
require 'faraday/api_auth'
191-
Faraday.new do |f|
192-
f.request :api_auth, @access_id, @secret_key
307+
class API < Grape::API
308+
helpers do
309+
def authenticate!
310+
error!('Unauthorized', 401) unless ApiAuth.authentic?(request, current_account.secret_key)
311+
end
312+
313+
def current_account
314+
@current_account ||= Account.find_by(access_id: ApiAuth.access_id(request))
315+
end
316+
end
317+
318+
before do
319+
authenticate!
320+
end
321+
322+
resource :protected do
323+
get do
324+
{ message: 'Authenticated!' }
325+
end
326+
end
193327
end
194328
```
195329
196-
The order of middlewares is important. You should make sure api_auth is last.
330+
### Rack Middleware
331+
332+
You can also implement ApiAuth as Rack middleware for any Rack-based application:
333+
334+
```ruby
335+
class ApiAuthMiddleware
336+
def initialize(app)
337+
@app = app
338+
end
339+
340+
def call(env)
341+
request = Rack::Request.new(env)
342+
343+
# Skip authentication for certain paths if needed
344+
return @app.call(env) if request.path == '/health'
345+
346+
# Find account by access ID
347+
access_id = ApiAuth.access_id(request)
348+
account = Account.find_by(access_id: access_id)
349+
350+
# Verify authenticity
351+
if account && ApiAuth.authentic?(request, account.secret_key)
352+
env['api_auth.account'] = account
353+
@app.call(env)
354+
else
355+
[401, { 'Content-Type' => 'text/plain' }, ['Unauthorized']]
356+
end
357+
end
358+
end
359+
360+
# In config.ru or Rails application.rb
361+
use ApiAuthMiddleware
362+
```
197363
198364
## Server
199365
@@ -272,6 +438,70 @@ def api_authenticate
272438
end
273439
```
274440
441+
## Digest Algorithms
442+
443+
ApiAuth supports multiple digest algorithms for generating signatures:
444+
445+
* SHA1 (default for backward compatibility)
446+
* SHA256 (recommended for new implementations)
447+
* SHA384
448+
* SHA512
449+
450+
To use a specific digest algorithm:
451+
452+
```ruby
453+
# Client side - signing
454+
ApiAuth.sign!(request, @access_id, @secret_key, digest: 'sha256')
455+
456+
# Server side - authenticating
457+
ApiAuth.authentic?(request, @secret_key, digest: 'sha256')
458+
```
459+
460+
When using a non-default digest, the Authorization header format changes to include the algorithm:
461+
462+
```
463+
Authorization: APIAuth-HMAC-SHA256 access_id:signature
464+
```
465+
466+
## Common Issues and Troubleshooting
467+
468+
### Clock Skew
469+
470+
If you're getting authentication failures, check the time synchronization between client and server. By default, requests are valid for 15 minutes. You can adjust this:
471+
472+
```ruby
473+
# Allow 60 seconds of clock skew
474+
ApiAuth.authentic?(request, secret_key, clock_skew: 60)
475+
```
476+
477+
### Content-Type Header
478+
479+
Ensure the Content-Type header is set before signing the request. The header is part of the canonical string used for signature generation.
480+
481+
### Request Path Encoding
482+
483+
The request path must be properly encoded. Special characters should be URL-encoded:
484+
485+
```ruby
486+
# Good
487+
'/api/users/john%40example.com'
488+
489+
# Bad
490+
'/api/users/john@example.com'
491+
```
492+
493+
### Debugging Failed Authentication
494+
495+
To debug authentication failures, you can compare the canonical strings:
496+
497+
```ruby
498+
# Get the canonical string from a request
499+
headers = ApiAuth::Headers.new(request)
500+
canonical_string = headers.canonical_string
501+
502+
# Compare client and server canonical strings to identify mismatches
503+
```
504+
275505
## Development
276506
277507
ApiAuth uses bundler for gem dependencies and RSpec for testing. Developing the
@@ -283,13 +513,13 @@ To run the tests:
283513
Install the dependencies for a particular Rails version by specifying a gemfile in `gemfiles` directory:
284514
285515
```sh
286-
BUNDLE_GEMFILE=gemfiles/rails_5.gemfile bundle install
516+
BUNDLE_GEMFILE=gemfiles/rails_7.gemfile bundle install
287517
```
288518
289519
Run the tests with those dependencies:
290520
291521
```sh
292-
BUNDLE_GEMFILE=gemfiles/rails_5.gemfile bundle exec rake
522+
BUNDLE_GEMFILE=gemfiles/rails_7.gemfile bundle exec rake
293523
```
294524
295525
If you'd like to add support for additional HTTP clients, check out the already

0 commit comments

Comments
 (0)