@@ -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.
8588ApiAuth supports many popular HTTP clients. Support for other clients can be
8689added 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
100107Here' s a sample implementation of signing a request created with RestClient.
101108
@@ -160,6 +167,116 @@ to:
160167Authorization = 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
165282ApiAuth 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
182299Flexirest::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
193327end
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
272438end
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
277507ApiAuth uses bundler for gem dependencies and RSpec for testing. Developing the
@@ -283,13 +513,13 @@ To run the tests:
283513Install 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
289519Run 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
295525If you' d like to add support for additional HTTP clients, check out the already
0 commit comments