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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +393 -0
- data/Rakefile +32 -0
- data/api_keys_dashboard.webp +0 -0
- data/api_keys_permissions.webp +0 -0
- data/api_keys_token.webp +0 -0
- data/app/controllers/api_keys/application_controller.rb +62 -0
- data/app/controllers/api_keys/keys_controller.rb +129 -0
- data/app/controllers/api_keys/security_controller.rb +16 -0
- data/app/views/api_keys/keys/_form.html.erb +106 -0
- data/app/views/api_keys/keys/_key_row.html.erb +72 -0
- data/app/views/api_keys/keys/_keys_table.html.erb +52 -0
- data/app/views/api_keys/keys/_show_token.html.erb +88 -0
- data/app/views/api_keys/keys/edit.html.erb +5 -0
- data/app/views/api_keys/keys/index.html.erb +26 -0
- data/app/views/api_keys/keys/new.html.erb +5 -0
- data/app/views/api_keys/keys/show.html.erb +12 -0
- data/app/views/api_keys/security/best_practices.html.erb +70 -0
- data/app/views/layouts/api_keys/application.html.erb +115 -0
- data/config/routes.rb +18 -0
- data/lib/api_keys/authentication.rb +160 -0
- data/lib/api_keys/configuration.rb +125 -0
- data/lib/api_keys/controller.rb +47 -0
- data/lib/api_keys/engine.rb +76 -0
- data/lib/api_keys/jobs/callbacks_job.rb +69 -0
- data/lib/api_keys/jobs/update_stats_job.rb +58 -0
- data/lib/api_keys/logging.rb +42 -0
- data/lib/api_keys/models/api_key.rb +209 -0
- data/lib/api_keys/models/concerns/has_api_keys.rb +144 -0
- data/lib/api_keys/services/authenticator.rb +255 -0
- data/lib/api_keys/services/digestor.rb +68 -0
- data/lib/api_keys/services/token_generator.rb +32 -0
- data/lib/api_keys/tenant_resolution.rb +40 -0
- data/lib/api_keys/version.rb +5 -0
- data/lib/api_keys.rb +49 -0
- data/lib/generators/api_keys/install_generator.rb +70 -0
- data/lib/generators/api_keys/templates/create_api_keys_table.rb.erb +100 -0
- data/lib/generators/api_keys/templates/initializer.rb +160 -0
- 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
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
|
+
[](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
|
+

|
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
|
+

|
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
|
+

|
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
|
data/api_keys_token.webp
ADDED
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
|