api_keys 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +393 -0
  5. data/Rakefile +32 -0
  6. data/api_keys_dashboard.webp +0 -0
  7. data/api_keys_permissions.webp +0 -0
  8. data/api_keys_token.webp +0 -0
  9. data/app/controllers/api_keys/application_controller.rb +62 -0
  10. data/app/controllers/api_keys/keys_controller.rb +129 -0
  11. data/app/controllers/api_keys/security_controller.rb +16 -0
  12. data/app/views/api_keys/keys/_form.html.erb +106 -0
  13. data/app/views/api_keys/keys/_key_row.html.erb +72 -0
  14. data/app/views/api_keys/keys/_keys_table.html.erb +52 -0
  15. data/app/views/api_keys/keys/_show_token.html.erb +88 -0
  16. data/app/views/api_keys/keys/edit.html.erb +5 -0
  17. data/app/views/api_keys/keys/index.html.erb +26 -0
  18. data/app/views/api_keys/keys/new.html.erb +5 -0
  19. data/app/views/api_keys/keys/show.html.erb +12 -0
  20. data/app/views/api_keys/security/best_practices.html.erb +70 -0
  21. data/app/views/layouts/api_keys/application.html.erb +115 -0
  22. data/config/routes.rb +18 -0
  23. data/lib/api_keys/authentication.rb +160 -0
  24. data/lib/api_keys/configuration.rb +125 -0
  25. data/lib/api_keys/controller.rb +47 -0
  26. data/lib/api_keys/engine.rb +76 -0
  27. data/lib/api_keys/jobs/callbacks_job.rb +69 -0
  28. data/lib/api_keys/jobs/update_stats_job.rb +58 -0
  29. data/lib/api_keys/logging.rb +42 -0
  30. data/lib/api_keys/models/api_key.rb +209 -0
  31. data/lib/api_keys/models/concerns/has_api_keys.rb +144 -0
  32. data/lib/api_keys/services/authenticator.rb +255 -0
  33. data/lib/api_keys/services/digestor.rb +68 -0
  34. data/lib/api_keys/services/token_generator.rb +32 -0
  35. data/lib/api_keys/tenant_resolution.rb +40 -0
  36. data/lib/api_keys/version.rb +5 -0
  37. data/lib/api_keys.rb +49 -0
  38. data/lib/generators/api_keys/install_generator.rb +70 -0
  39. data/lib/generators/api_keys/templates/create_api_keys_table.rb.erb +100 -0
  40. data/lib/generators/api_keys/templates/initializer.rb +160 -0
  41. metadata +184 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7d5beb5e03e8d5bc749e2a05456c2b0ac0105fb9ee7be6bb1c15e0a47800f759
4
+ data.tar.gz: 1da205c1b54d157cf01ef194382eae9e69b8cb1ec920f1497ab791438ad30408
5
+ SHA512:
6
+ metadata.gz: be22eed1e97b88042860f23968f3a6c84cd1ec8e60ff8547f097293d456d743fc3ad819c20696357ba3453b60fbcb3ff1e81f593160ce7b4e86e71e6777a3f8c
7
+ data.tar.gz: 22061fc8c3fe051cb2104dfd3e19a85226469c85bdd0190b7f22291b8e44d4d0cfacbc34dbfe8168c1ce61983f1dcb229bdac2deb220826eac11acbfd23e789d
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## [0.1.0] - 2025-04-30
2
+
3
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Javi R
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,393 @@
1
+ # 🔑 `api_keys` – Gate your Rails API with secure, self-serve API keys
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/pay.svg)](https://badge.fury.io/rb/pay)
4
+
5
+ `api_keys` makes it dead simple to add secure, production-ready API key authentication to your Rails app. Generate keys, restrict scopes, auto-expire tokens, revoke tokens. It also provides a self-serve dashboard for your users to self-issue and manage their API keys themselves. All tokens are hashed securely by default, and never stored in plaintext.
6
+
7
+ [ 🟢 [Live interactive demo website](https://apikeys.rameerez.com) ]
8
+
9
+ ![API Keys Dashboard](api_keys_dashboard.webp)
10
+
11
+ Check out my other 💎 Ruby gems: [`allgood`](https://github.com/rameerez/allgood) · [`usage_credits`](https://github.com/rameerez/usage_credits) · [`profitable`](https://github.com/rameerez/profitable) · [`nondisposable`](https://github.com/rameerez/nondisposable)
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem "api_keys"
19
+ ```
20
+
21
+ And then `bundle install`. After the gem is installed, run the generator and migration:
22
+
23
+ ```bash
24
+ rails g api_keys:install
25
+ rails db:migrate
26
+ ```
27
+
28
+ And you're done!
29
+
30
+ ## Quick Start
31
+
32
+ Just add `has_api_keys` to your desired model. For example, if you want your `User` records to have API keys, you'd have:
33
+
34
+ ```ruby
35
+ class User < ApplicationRecord
36
+ has_api_keys
37
+ end
38
+ ```
39
+
40
+ You can also customize how many maximum keys your users can have by passing a block to `has_api_keys`, like this:
41
+
42
+ ```ruby
43
+ class User < ApplicationRecord
44
+ has_api_keys do
45
+ max_keys 10 # only 10 active API keys per user allowed
46
+ require_name true # always require users to set a name for each API key
47
+ end
48
+ end
49
+ ```
50
+
51
+ It'd work the same if you want your `Organization` or your `Project` records to have API keys.
52
+
53
+ ### Mount the dashboard engine
54
+
55
+ The goal of `api_keys` is to allow you to turn your Rails app into an API platform with secure key authentication in minutes, as in: drop in this gem and you're pretty much done.
56
+
57
+ To achieve that, the gem provides a ready-to-go dashboard you can just mount in your `routes.rb` like this:
58
+
59
+ ```ruby
60
+ mount ApiKeys::Engine => '/settings/api-keys'
61
+ ```
62
+
63
+ Now your users can:
64
+ - self-issue new API keys
65
+ - set expiration dates
66
+ - attach scopes / permissions to individual keys
67
+ - add and edit the key names
68
+ - revoke instantly
69
+ - see the status of all their keys
70
+
71
+ It provides an UI with everything you'd expect from an API keys dashboard, working right out of the box:
72
+
73
+ ![API Keys Dashboard](api_keys_token.webp)
74
+
75
+ To make the experience between your app and the `api_keys` dashboard more seamless, you can configure a `return_url` and `return_text` so your users can quickly go back to your app or settings page (in the screenshot above, that's the "Home" links, text customizable)
76
+
77
+ You can check out the dashboard on the [live demo website](https://apikeys.rameerez.com).
78
+
79
+
80
+ ## How it works
81
+
82
+ ### Issuing new API keys
83
+
84
+ If you want to write your own front-end instead of using the provided dashboard, or just want to issue API keys at any point, you can do it with `create_api_key!`:
85
+
86
+ ```ruby
87
+ @api_key = @user.create_api_key!(
88
+ name: "my-key",
89
+ scopes: "['read', 'write']",
90
+ expires_at: 42.days.from_now
91
+ )
92
+
93
+ # Get the plaintext token only available upon creation
94
+ plaintext_token = @api_key.token
95
+ # => ak_123abc...
96
+ ```
97
+
98
+ For security reasons, the **gem does not store the generated key** in the database.
99
+
100
+ We only store a salted hash, so the API key / API token itself is only available in plaintext immediately after creation, as `@api_key.token` – the `.token` method won't work any other time.
101
+
102
+ With this token, your users can make calls to your endpoints by attaching it as an `"Authorization: Bearer ak_123abc..."` in their HTTP calls headers, like this:
103
+
104
+ ```bash
105
+ curl -X GET -H "Authorization: Bearer ak_123abc..." "http://example.com/api/endpoint"
106
+ ```
107
+
108
+ ### Listing all keys for users
109
+
110
+ Of course, you can list all API keys for any record like this:
111
+ ```ruby
112
+ @user.api_keys
113
+ ```
114
+
115
+ You can filter by active keys, expired keys, revoked keys:
116
+ ```ruby
117
+ @user.api_keys.active
118
+ @user.api_keys.expired
119
+ @user.api_keys.revoked
120
+ @user.api_keys.inactive # expired or revoked
121
+ ```
122
+
123
+ ### Useful API key methods
124
+
125
+ Check if an API key is still active and therefore allowed to perform actions:
126
+ ```ruby
127
+ @api_key.active?
128
+ # => true
129
+ ```
130
+
131
+ Or expired:
132
+ ```ruby
133
+ @api_key.expired?
134
+ # => false
135
+ ```
136
+
137
+ You can revoke (disable, make inactive) any API key at any point like this:
138
+ ```ruby
139
+ @api_key.revoke!
140
+ ```
141
+
142
+ And you can check if an API key is revoked like this:
143
+ ```ruby
144
+ @api_key.revoked?
145
+ # => true
146
+ ```
147
+
148
+ And for any API key, you can always display a safe, user-friendly masked token to display on user interfaces so users can easily identify their keys:
149
+ ```ruby
150
+ @api_key.masked_token
151
+ # => "ak_demo_••••yZn9"
152
+ ```
153
+
154
+ ### Scopes: define and verify API Key permissions
155
+
156
+ Users can limit what each API key does by selecting scopes, and you can define those scopes.
157
+
158
+ In the `config/initializers/api_keys.rb` initializer generated when you installed the gem, you'll find an option to define global scopes:
159
+ ```ruby
160
+ config.default_scopes = ["read", "write"]
161
+ ```
162
+
163
+ These will be the available permissions you'll see, for example, in the API Keys dashboard:
164
+
165
+ ![API Keys Dashboard](api_keys_permissions.webp)
166
+
167
+ You can also define per-model scopes by passing the option to the `has_api_keys` block, which overrides global defaults:
168
+
169
+ ```ruby
170
+ class User < ApplicationRecord
171
+ has_api_keys do
172
+ max_keys 10
173
+ default_scopes %w[read write admin]
174
+ end
175
+ end
176
+ ```
177
+
178
+ You can get as granular with your scopes as you'd like, think for example AWS-like strings of the form: `"s3:GetObject"` – how you set this up is up to you! Scopes take any string: we recommend sticking to simple verbs (`"read"`, `"write"`) or `"resource:action"` (case-sensitive!)
179
+
180
+ You can check if an API key is allowed to do actions by checking its scopes:
181
+ ```ruby
182
+ @api_key.allows_scope?("read")
183
+ # => true
184
+ ```
185
+
186
+ ## Controllers: secure your API endpoints
187
+
188
+ To add the `api_keys` functionality to your controllers, just use the `ApiKeys::Controller` concern and you'll have all controller methods available:
189
+ ```ruby
190
+ class ApiController < ApplicationController
191
+ include ApiKeys::Controller # provides authenticate_api_key! and current_api_key_owner
192
+ end
193
+ ```
194
+
195
+ With this, you get the `authenticate_api_key!` and `current_api_key_owner` methods, which come in handy to build your key-gated actions.
196
+
197
+ ### Require an API key for an endpoint
198
+
199
+ If you just want to check the presence of a valid (active, non-expired, non-revoked) API key for an endpoint, you can do:
200
+
201
+ ```ruby
202
+ before_action :authenticate_api_key!
203
+ ```
204
+
205
+ And of course, if you want to have unauthenticated endpoints:
206
+
207
+ ```ruby
208
+ before_action :authenticate_api_key!, except: [:unauthenticated_endpoint]
209
+ ```
210
+
211
+ `authenticate_api_key!` will return `401 Unauthenticated` for anything that's not a valid API key.
212
+
213
+ It will also load the valid API key, if any, to a `current_api_key` variable, that returns an API Key object (`ApiKeys::ApiKey`) on which you can call all the methods we've outlined above, and access any attribute, like:
214
+
215
+ ```ruby
216
+ current_api_key.expires_at
217
+ # => 2025-05-25 05:25:05.250525000 UTC +00:00
218
+ ```
219
+
220
+ If the API key has an owner, you can also access it either with `current_api_key.owner` or with the helper method `current_api_key_owner`
221
+
222
+ For example, if the owner of the API key is a `User`, you might do something like:
223
+ ```ruby
224
+ current_api_key_owner.email
225
+ # => john.doe@example.com
226
+ ```
227
+
228
+ ### Require a scope for an endpoint
229
+
230
+ You can require a specific scope for any endpoint like:
231
+ ```ruby
232
+ authenticate_api_key!(scope: "write")
233
+ ```
234
+
235
+ It may be cleaner if you pass it as a Proc to `before_action` – and it may result in better-organized code if you do it endpoint-per-endpoint, immediately before each method definition, like this:
236
+
237
+ ```ruby
238
+ before_action -> { authenticate_api_key!(scope: "write") }, only: [:write_action]
239
+ def write_action
240
+ # We'll only get here if the API key is active AND it has the right scope, so execute the actual logic of the endpoint and return success:
241
+ render json: {
242
+ # Your success JSON...
243
+ }, status: :ok
244
+ end
245
+ ```
246
+
247
+ ### Rate limit your API endpoints
248
+
249
+ Rails 8 introduced the native, built-in `rate_limit` to easily rate limit your endpoints, so `Rack::Attack` is no longer necessary! While this is not an `api_keys` feature per se, I thought it'd be nice to include an example here because it pairs so well with `api_keys`.
250
+
251
+ For example, if you want to rate limit an endpoint to only accept 2 requests each 10 seconds, per API key, you'd do something like:
252
+ ```ruby
253
+ before_action -> { authenticate_api_key! }, only: [:rate_limited_action]
254
+ rate_limit to: 2, within: 10.seconds,
255
+ by: -> { current_api_key&.id }, # Limit per API key ID
256
+ with: -> { render json: { error: "rate_limited", message: "Too many requests (max 2 per 10 seconds per key). Please wait." }, status: :too_many_requests },
257
+ only: [:rate_limited_action]
258
+ def rate_limited_action
259
+ render json: {
260
+ # Success JSON
261
+ }, status: :ok
262
+ end
263
+ ```
264
+
265
+ This `rate_limit` feature depends on Rails 8+ and an active, well-configured cache store, like [`solid_cache`](https://github.com/rails/solid_cache), which comes by default in Rails 8.
266
+
267
+ If you're still on early versions of Rails, you can still use `api_keys`! No need to implement `rate_limit` – just an idea if you're already on Rails 8!
268
+
269
+
270
+ ## Configuration and settings
271
+
272
+ The gem installation creates an initializer at `config/initializers/api_keys.rb`
273
+
274
+ The default initializer is self-explanatory and self-documented, please consider spending a bit of time reading through it if you want to fine-tune the gem.
275
+
276
+ Some highlights:
277
+
278
+ ### Accept API keys via query params instead of Authentication HTTP headers
279
+
280
+ By default, the `api_key` gem expects API keys to come *exclusively* as HTTP Authentication Bearer tokens, for security purposes. But you can allow users to make requests to your endpoints with the API key token passed as a URL query param too, like this:
281
+
282
+ ```
283
+ https://example.com/api/endpoint?api_key=ak_123abc...
284
+ ```
285
+
286
+ This is not recommended security-wise because you'll be leaking API tokens everywhere in your logs, but if you want to enable this, just set the query param name you're expecting the API key token to be in:
287
+
288
+ ```ruby
289
+ config.query_param = "api_key"
290
+ ```
291
+
292
+ ### Changing the hashing function to `bcrypt` for maximum security
293
+
294
+ By default, the `api_keys` gem hashes tokens using `sha256`, for fast token lookup and low-latency API authentication. Tokens are salted via their prefix, and only stored as secure digests.
295
+
296
+ If you need slower, password-grade hashing (e.g., for extremely sensitive tokens), you can switch to bcrypt:
297
+
298
+ ```ruby
299
+ config.hash_strategy = :bcrypt
300
+ ```
301
+
302
+ Note: bcrypt is ~50–100x slower than SHA256. For most API use cases, sha256 is more than sufficient.
303
+
304
+ `sha256` has O(1) lookup, `bcrypt` doesn't. This means that if you switch to `bcrypt`, you may observe ~100ms lags on every API call, for every token auth that's not cached.
305
+
306
+ For 99% of APIs, `sha256` is more than secure enough — and far better for performance.
307
+
308
+ ### Increase cache TTL
309
+
310
+ We cache token lookups to improve performance, especially for repeated requests. This keeps `bcrypt` and `sha256` strategies fast under load.
311
+
312
+ By default, we use a 5-second TTL, which offers a strong balance: most requests benefit from caching, while revoked keys stop working almost immediately.
313
+
314
+ If security is your top priority (e.g. rapid revocation after suspected key compromise), you can disable caching entirely:
315
+
316
+ ```ruby
317
+ config.cache_ttl = 0.seconds # disables caching
318
+ ```
319
+
320
+ If performance matters more than real-time revocation, increase the TTL to reduce DB hits:
321
+
322
+ ```ruby
323
+ config.cache_ttl = 2.minutes # boosts performance at cost of slower revocation
324
+ ```
325
+
326
+ ⚠️ Security note: Revoked keys may remain valid for up to cache_ttl. For strict real-time revocation, set cache_ttl = 0.
327
+
328
+
329
+ ## Callbacks: analytics, logging, usage monitoring & auditing
330
+
331
+ The gem offers two callbacks that get executed every single time an API key is checked and authenticated (through `authenticate_api_key!` in controllers, for example)
332
+
333
+ You can define logic for them:
334
+ ```ruby
335
+ config.before_authentication = ->(request) { Rails.logger.info "Authenticating request: #{request.uuid}" }
336
+
337
+ config.after_authentication = ->(result) { MyAnalytics.track_auth(result) }
338
+ ```
339
+
340
+ This is especially useful if you want to build custom monitoring, usage tracking or auditing systems on top of the `api_keys` gem.
341
+
342
+ Since these callbacks get called every single time an endpoint request is made, we can't just execute the code synchronously, blocking the thread and making the endpoint lag. Instead, we enqueue an async job that process the callback code, however long it is. You can configure which queue these jobs get enqueued to.
343
+
344
+ The downside of this, of course, is that callbacks will only work if you have a valid, well-configured Active Job backend for your Rails app, like Sidekiq or [`solid_queue`](https://github.com/rails/solid_queue/), which comes by default in Rails 8. If Active Job is not well configured, well, your callbacks just won't get executed.
345
+
346
+ There's also a `track_requests_count` config option that you can turn on so the gem keeps track of how many requests has each API key made. When this is on, you may access the count like this:
347
+ ```ruby
348
+ @api_key.requests_count
349
+ # => 4567
350
+ ```
351
+
352
+ But again, this is turned off by default for performance purposes, and depends on having a working, well-configured Active Job backend.
353
+
354
+ ## Enterprise-ready by design
355
+ The `api_keys` gem ships with:
356
+
357
+ - Flexible storage
358
+ - Async hooks
359
+ - ActiveJob support
360
+ - Polymorphic ownership (User, Org, etc.)
361
+ - Custom scopes
362
+ - Production caching
363
+ - Tracking of last time each key was used
364
+ - Usage tracking
365
+ - SHA256 fallback
366
+
367
+ Making it enterprise-ready, built with extensibility and compliance in mind.
368
+
369
+ ## Roadmap
370
+ - [ ] Automatic rotation policy helpers
371
+ - [ ] Key-pair / HMAC option
372
+
373
+ ## Demo Rails app
374
+
375
+ There's a demo Rails app showcasing the features in the `api_keys` gem under `test/dummy`. It's currently deployed to `apikeys.rameerez.com`. If you want to run it yourself locally, you can just clone this repo, `cd` into the `test/dummy` folder, and then `bundle` and `rails s` to launch it. You can examine the code of the demo app to better understand the gem.
376
+
377
+ ## Testing
378
+
379
+ Run the test suite with `bundle exec rake test`
380
+
381
+ ## Development
382
+
383
+ 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.
384
+
385
+ To install this gem onto your local machine, run `bundle exec rake install`.
386
+
387
+ ## Contributing
388
+
389
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rameerez/api_keys. Our code of conduct is: just be nice and make your mom proud of what you do and post online.
390
+
391
+ ## License
392
+
393
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ begin
2
+ require "bundler/setup"
3
+ rescue LoadError
4
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
5
+ end
6
+
7
+ require "bundler/gem_tasks"
8
+
9
+ require "rdoc/task"
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = "rdoc"
13
+ rdoc.title = "Pay"
14
+ rdoc.options << "--line-numbers"
15
+ rdoc.rdoc_files.include("README.md")
16
+ rdoc.rdoc_files.include("lib/**/*.rb")
17
+ end
18
+
19
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
20
+ load "rails/tasks/engine.rake"
21
+
22
+ load "rails/tasks/statistics.rake"
23
+
24
+ require "rake/testtask"
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << "test"
28
+ t.pattern = "test/**/*_test.rb"
29
+ t.verbose = false
30
+ end
31
+
32
+ task default: :test
Binary file
Binary file
Binary file
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiKeys
4
+ # Base controller for the ApiKeys engine.
5
+ # Inherits from the host application's configured controller
6
+ # (defaults to ::ApplicationController).
7
+ # Includes common engine functionality.
8
+ class ApplicationController < ApiKeys::Engine.config.parent_controller.constantize
9
+ # Protect from forgery if the parent controller does
10
+ # This ensures CSRF protection behaves consistently with the host app.
11
+ protect_from_forgery with: :exception if respond_to?(:protect_from_forgery)
12
+
13
+ # Include the main controller concern which bundles authentication and tenant resolution
14
+ include ApiKeys::Controller
15
+
16
+ # Ensure user is authenticated for all actions within this engine
17
+ # IMPORTANT: This assumes the host application provides a `current_user` method
18
+ # and potentially a `authenticate_user!` method (like Devise).
19
+ # You might need to adjust this based on the host app's authentication system.
20
+ before_action :authenticate_api_keys_user!
21
+
22
+ private
23
+
24
+ # Placeholder method to authenticate the user accessing the engine.
25
+ # Relies on the host application providing `authenticate_user!` and `current_user`.
26
+ # Developers might need to override this in their application or configure
27
+ # the authentication method if it differs from standard Devise/similar patterns.
28
+ def authenticate_api_keys_user!
29
+ # Try common authentication methods
30
+ if defined?(authenticate_user!)
31
+ authenticate_user!
32
+ elsif defined?(require_login)
33
+ require_login # Common in Sorcery
34
+ else
35
+ # Fallback or raise error if no known authentication method is found
36
+ unless current_api_keys_user
37
+ # Redirect or render error if no user context is available
38
+ # Choose the appropriate action based on expected host app behavior
39
+ redirect_to main_app.root_path, alert: "You need to sign in or sign up before continuing." rescue render plain: "Unauthorized", status: :unauthorized
40
+ end
41
+ end
42
+ end
43
+
44
+ # Helper method to access the current user from the host application.
45
+ # Assumes the host app provides `current_user`.
46
+ def current_api_keys_user
47
+ # Use `super` if the parent controller defines `current_user`
48
+ # Otherwise, try calling `current_user` directly on self if it's mixed in.
49
+ if defined?(super)
50
+ super
51
+ elsif defined?(current_user)
52
+ current_user
53
+ else
54
+ nil # No user context found
55
+ end
56
+ end
57
+
58
+ # Expose current_api_keys_user as a helper method for views
59
+ helper_method :current_api_keys_user
60
+
61
+ end
62
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiKeys
4
+ # Controller for managing API keys belonging to the current user.
5
+ class KeysController < ApplicationController
6
+ before_action :set_api_key, only: [:show, :edit, :update, :revoke]
7
+
8
+ # GET /keys
9
+ def index
10
+ # Fetch only active keys for the main list, maybe sorted by creation date
11
+ @api_keys = current_api_keys_user.api_keys.active.order(created_at: :desc)
12
+ # Optionally, fetch inactive ones for a separate section or filter
13
+ @inactive_api_keys = current_api_keys_user.api_keys.inactive.order(created_at: :desc)
14
+ end
15
+
16
+ # GET /keys/:id
17
+ # Shows the newly generated key's plaintext token ONCE.
18
+ # This is not a standard show action, it's used transiently after creation.
19
+ def show
20
+ # Key is set by set_api_key
21
+ # We need to retrieve the plaintext token stored temporarily
22
+ # after creation. This relies on how we handle creation.
23
+ # We'll likely store it in the session flash or pass it directly.
24
+ @plaintext_token = session.delete(:plaintext_api_key) # Retrieve and delete from session
25
+ unless @plaintext_token
26
+ # If accessed directly without the token, redirect or show an error
27
+ redirect_to keys_path, alert: "API key token can only be shown once immediately after creation."
28
+ end
29
+ end
30
+
31
+ # GET /keys/new
32
+ def new
33
+ @api_key = current_api_keys_user.api_keys.build
34
+ end
35
+
36
+ # POST /keys
37
+ def create
38
+ # Use the HasApiKeys helper method to create the key
39
+ begin
40
+ # create_api_key! now returns the ApiKey instance
41
+ @api_key = current_api_keys_user.create_api_key!(
42
+ name: api_key_params[:name],
43
+ scopes: api_key_params[:scopes],
44
+ expires_at: parse_expiration(api_key_params[:expires_at_preset])
45
+ # Metadata could be added here if needed
46
+ )
47
+
48
+ # Get the plaintext token from the instance's attr_reader
49
+ plaintext_token = @api_key.token
50
+
51
+ # Store the plaintext token in session to display on the show page
52
+ session[:plaintext_api_key] = plaintext_token
53
+
54
+ redirect_to key_path(@api_key)
55
+ rescue ActiveRecord::RecordInvalid => e
56
+ # If create! fails due to validation (e.g., quota exceeded)
57
+ @api_key = e.record # Get the invalid ApiKey instance
58
+ flash.now[:alert] = "Failed to create API key: #{e.record.errors.full_messages.join(', ')}"
59
+ render :new, status: :unprocessable_entity
60
+ rescue => e # Catch other potential errors
61
+ flash.now[:alert] = "An unexpected error occurred: #{e.message}"
62
+ @api_key = current_api_keys_user.api_keys.build(api_key_params) # Rebuild form
63
+ render :new, status: :unprocessable_entity
64
+ end
65
+ end
66
+
67
+ # GET /keys/:id/edit
68
+ def edit
69
+ # Key is set by set_api_key
70
+ end
71
+
72
+ # PATCH/PUT /keys/:id
73
+ def update
74
+ if @api_key.update(api_key_update_params)
75
+ redirect_to keys_path, notice: "API key updated successfully."
76
+ else
77
+ flash.now[:alert] = "Failed to update API key: #{@api_key.errors.full_messages.join(', ')}"
78
+ render :edit, status: :unprocessable_entity
79
+ end
80
+ end
81
+
82
+ # POST /keys/:id/revoke
83
+ def revoke
84
+ if @api_key.revoke!
85
+ redirect_to keys_path, notice: "API key revoked successfully."
86
+ else
87
+ # This shouldn't typically fail unless there's a callback issue
88
+ redirect_to keys_path, alert: "Failed to revoke API key."
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ # Use callbacks to share common setup or constraints between actions.
95
+ def set_api_key
96
+ @api_key = current_api_keys_user.api_keys.find(params[:id])
97
+ rescue ActiveRecord::RecordNotFound
98
+ redirect_to keys_path, alert: "API key not found."
99
+ end
100
+
101
+ # Only allow a list of trusted parameters through for creation.
102
+ # Added :expires_at_preset for the dropdown selector.
103
+ def api_key_params
104
+ permitted_params = params.require(:api_key).permit(:name, :expires_at_preset, scopes: [])
105
+ permitted_params[:scopes]&.reject!(&:blank?) # Filter out blank strings
106
+ permitted_params
107
+ end
108
+
109
+ # Only allow updating name and scopes.
110
+ def api_key_update_params
111
+ permitted_params = params.require(:api_key).permit(:name, scopes: [])
112
+ permitted_params[:scopes]&.reject!(&:blank?) # Filter out blank strings
113
+ permitted_params
114
+ end
115
+
116
+ # Helper to parse the expiration preset string into a Time object
117
+ def parse_expiration(preset)
118
+ case preset
119
+ when "7_days" then 7.days.from_now
120
+ when "30_days" then 30.days.from_now
121
+ when "60_days" then 60.days.from_now
122
+ when "90_days" then 90.days.from_now
123
+ when "365_days" then 365.days.from_now
124
+ when "no_expiration" then nil
125
+ else nil # Default to no expiration if invalid preset
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiKeys
4
+ # Controller for static informational pages within the engine.
5
+ class SecurityController < ApplicationController
6
+ # Skip the user authentication requirement for these static pages
7
+ # as they contain general information.
8
+ skip_before_action :authenticate_api_keys_user!, only: [:best_practices]
9
+
10
+ # GET /security/best-practices
11
+ def best_practices
12
+ # Renders app/views/api_keys/security/best_practices.html.erb
13
+ # The view will contain the static content.
14
+ end
15
+ end
16
+ end