api_keys 0.3.0 → 0.4.1
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 +4 -4
- data/CHANGELOG.md +68 -0
- data/README.md +147 -38
- data/SECURITY.md +33 -0
- data/app/controllers/api_keys/application_controller.rb +92 -10
- data/app/controllers/api_keys/keys_controller.rb +40 -18
- data/app/views/api_keys/keys/_empty_state.html.erb +1 -1
- data/app/views/api_keys/keys/_form.html.erb +3 -3
- data/app/views/api_keys/keys/_key_actions.html.erb +3 -3
- data/app/views/api_keys/keys/_key_badges.html.erb +2 -2
- data/app/views/api_keys/keys/_key_row.html.erb +1 -1
- data/app/views/api_keys/keys/_key_status.html.erb +3 -3
- data/app/views/api_keys/keys/_keys_table.html.erb +1 -4
- data/app/views/api_keys/keys/_show_token.html.erb +5 -46
- data/app/views/api_keys/keys/_token_display.html.erb +3 -3
- data/app/views/api_keys/keys/index.html.erb +2 -2
- data/app/views/api_keys/keys/show.html.erb +2 -2
- data/app/views/api_keys/security/best_practices.html.erb +7 -7
- data/app/views/layouts/api_keys/application.html.erb +159 -12
- data/lib/api_keys/authentication.rb +39 -11
- data/lib/api_keys/configuration.rb +412 -24
- data/lib/api_keys/engine.rb +5 -20
- data/lib/api_keys/form_builder_extensions.rb +12 -2
- data/lib/api_keys/helpers/expiration_options.rb +11 -3
- data/lib/api_keys/helpers/token_session.rb +143 -8
- data/lib/api_keys/helpers/view_helpers.rb +5 -1
- data/lib/api_keys/jobs/callbacks_job.rb +10 -17
- data/lib/api_keys/jobs/update_stats_job.rb +27 -12
- data/lib/api_keys/models/api_key.rb +244 -25
- data/lib/api_keys/models/concerns/has_api_keys.rb +95 -32
- data/lib/api_keys/services/authenticator.rb +263 -118
- data/lib/api_keys/services/digestor.rb +76 -13
- data/lib/api_keys/services/token_generator.rb +41 -1
- data/lib/api_keys/tenant_resolution.rb +2 -4
- data/lib/api_keys/version.rb +1 -1
- data/lib/generators/api_keys/add_authentication_index_generator.rb +36 -0
- data/lib/generators/api_keys/templates/add_authentication_index_to_api_keys.rb.erb +24 -0
- data/lib/generators/api_keys/templates/create_api_keys_table.rb.erb +2 -14
- data/lib/generators/api_keys/templates/initializer.rb +54 -17
- metadata +16 -16
- data/.simplecov +0 -36
- data/AGENTS.md +0 -5
- data/Appraisals +0 -17
- data/CLAUDE.md +0 -5
- data/Rakefile +0 -37
- data/context7.json +0 -4
- data/gemfiles/rails_7.2.gemfile +0 -21
- data/gemfiles/rails_8.0.gemfile +0 -21
- data/gemfiles/rails_8.1.gemfile +0 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 51db6ca1f89acc5b0638e33db41b88694b8be6275c3528a72012b3e2609d8736
|
|
4
|
+
data.tar.gz: 40c384e1af9d2befca96c4c5a3395daff9059ee19334ee01f997c59396367bdd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8ba7f9f769475fbf703446fa91ba19500393391eb1f1fa9b45eb5772fb46e044f8af492058d306c64bcf657c0c3654846d1d188916e4edaf7ee9e9a68571deed
|
|
7
|
+
data.tar.gz: f6ede6c7e458b1cdbac9cd580e46536ba735891e283c4a4e2eececbc4c194b91e36ca5756a43cf8b3ef80c9d5066fa50d35cdc59efa246543bbc5e85b3ebd352
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,71 @@
|
|
|
1
|
+
## [0.4.1] - 2026-08-24
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- Stop the dashboard's Content Security Policy from blocking the host application's own stylesheets, scripts, and self-hosted webfonts. The 0.4.0 policy declared `default-src 'none'` with `script-src`/`style-src` reduced to the per-request nonce, but Rails only stamps that nonce onto `stylesheet_link_tag`/`javascript_include_tag` when the host application sets `content_security_policy_nonce_auto` (off by default, and not something an engine can enable for its host). Any application rendering its normal layout on engine pages got an unstyled, inert dashboard with no server-side error. The default policy now trusts same-origin scripts, styles, and fonts, adds `font-src`, and allows `https:` images, while keeping the nonce, `default-src 'self'`, `base-uri 'none'`, `object-src 'none'`, `frame-ancestors 'none'`, `frame-src 'none'`, `form-action 'self'`, and `connect-src 'self'`.
|
|
6
|
+
- Stop generated install and authentication-index migrations from carrying an unreachable `migration_version` instance method; the generator already renders the Active Record version into each migration superclass.
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `config.dashboard_content_security_policy` selects the policy the mounted dashboard declares: `:default` (new default, hardened but compatible with a normal host layout), `:strict` (the 0.4.0 nonce-only policy, unchanged), or `false`/`nil` to declare nothing and leave the host application's policy alone. The setting is resolved per request and validated on assignment.
|
|
11
|
+
|
|
12
|
+
### Upgrade notes
|
|
13
|
+
|
|
14
|
+
- Applications that relied on the 0.4.0 nonce-only dashboard policy should set `config.dashboard_content_security_policy = :strict` to keep it. That policy requires a layout that serves nothing un-nonced on engine pages; the gem's built-in layout satisfies it, and host layouts additionally need `config.content_security_policy_nonce_auto = true`.
|
|
15
|
+
|
|
16
|
+
### Maintenance
|
|
17
|
+
|
|
18
|
+
- Move SimpleCov startup/reporting into the test helper and replace deprecated filtering/tracking APIs so the enforced line and branch coverage gates remain compatible with future SimpleCov releases.
|
|
19
|
+
|
|
20
|
+
## [0.4.0] - 2026-08-09
|
|
21
|
+
|
|
22
|
+
This security-focused release hardens authentication, authorization, credential handling, the self-serve dashboard, background jobs, configuration, dependencies, CI, and the release supply chain. Upgrading is strongly recommended. Existing installations must apply the authentication lookup migration before deploying this version.
|
|
23
|
+
|
|
24
|
+
### Security
|
|
25
|
+
|
|
26
|
+
- Make token caching a non-authoritative ID lookup hint: every hit reloads current database state and cryptographically re-verifies the presented token, so revocation, expiration, scope changes, and cache poisoning fail closed.
|
|
27
|
+
- Replace token-derived SHA1 cache identifiers with SHA256 and prevent plaintext tokens, digests, stored public tokens, request objects, and authentication result objects from leaking through gem logs, inspection, serialization, errors, or async callback arguments.
|
|
28
|
+
- Bound token sizes, bcrypt input length, bcrypt cost, and bcrypt candidate work; add an indexed prefix/last-four/algorithm lookup for existing installations.
|
|
29
|
+
- Bound historical-prefix discovery and fall back to an indexed last-four lookup so cache poisoning or unusually many retired prefixes cannot create unbounded request work or strand valid bcrypt keys.
|
|
30
|
+
- Serialize all quota checks and inserts by locking the owner row, including direct Active Record creation paths.
|
|
31
|
+
- Enforce immutable authentication identity fields, supported digest formats, bounded scopes/metadata/identifiers/session payloads, runtime permission ceilings, configured key types, and fail-closed environment isolation.
|
|
32
|
+
- Require every newly created key to have a configured type once key-types mode is enabled; existing untyped legacy keys remain compatible.
|
|
33
|
+
- Require explicit finite permissions and `revocable: false` for intentionally stored public tokens.
|
|
34
|
+
- Fail closed when dashboard owner authentication is missing or ineffective; bind one-time session tokens to their created key; reject malformed create/update payloads; scope every dashboard lookup to the current owner; and add no-store, anti-framing, referrer, MIME-sniffing, and permissions headers.
|
|
35
|
+
- Apply an enforcing nonce-based dashboard Content Security Policy and remove inline handlers, un-nonced scripts, third-party assets, and inline style attributes from credential-bearing views.
|
|
36
|
+
- Enforce HTTPS authentication by default in production, disable query-string credentials in the demo, and remove third-party code from pages that handle tokens.
|
|
37
|
+
- Pin GitHub Actions to immutable commits and add dependency auditing, Brakeman, CodeQL, dependency review, and Dependabot configuration.
|
|
38
|
+
- Remove the unused Claude Code workflow and its third-party CI/OIDC surface.
|
|
39
|
+
- Encrypt one-time secret-token handoffs inside the Rails session with an application-derived AES-256-GCM key, bind them to the created key, expire them after ten minutes, and delete them on first retrieval. Legacy in-flight plaintext handoffs remain readable for upgrade continuity but are never newly written.
|
|
40
|
+
|
|
41
|
+
### Reliability
|
|
42
|
+
|
|
43
|
+
- Execute authentication callbacks exactly once with serializable, credential-free context hashes.
|
|
44
|
+
- Resolve job queues dynamically, prevent out-of-order/future jobs from corrupting usage timestamps, and keep request counters atomic.
|
|
45
|
+
- Validate security-sensitive global and per-owner configuration early and store permission policy as defensive frozen copies.
|
|
46
|
+
- Ensure each appraisal excludes the next Rails line's prereleases; Rails 7.2 uses its upstream-compatible Minitest 5 while Rails 8/default suites remain on Minitest 6.
|
|
47
|
+
- Return `403 Forbidden` when a valid key lacks a required scope while retaining `401 Unauthorized` for missing or invalid credentials.
|
|
48
|
+
- Honor the public `parent_controller` configuration (with a backward-compatible internal Engine fallback), cascade owner deletion across non-revocable keys, and debounce `last_used_at` jobs for one minute by default when exact request counting is disabled.
|
|
49
|
+
- Remove redundant single-column polymorphic-owner indexes from new-install migrations and exclude development-only metadata from built gems.
|
|
50
|
+
- Resolve every finding recorded in [issue #12](https://github.com/rameerez/api_keys/issues/12) through a code fix, test, explicit bounded design, or superseding hardening control.
|
|
51
|
+
|
|
52
|
+
### Upgrade notes
|
|
53
|
+
|
|
54
|
+
- Existing installations should run `rails generate api_keys:add_authentication_index` and `rails db:migrate` before deploying this version.
|
|
55
|
+
- Invalid expiration presets now raise `ArgumentError` instead of silently creating a key without expiration.
|
|
56
|
+
- Authentication identity fields (`token_digest`, algorithm, prefix, last four, owner, key type, and environment) can no longer be changed through normal Active Record updates after creation.
|
|
57
|
+
- Typed keys with blank or retired environments now fail closed. Repair any such legacy rows before deployment; untyped legacy keys are unaffected.
|
|
58
|
+
- When `key_types` is configured, new keys must pass `key_type:` or use a configured `default_key_type`; this does not invalidate existing untyped keys.
|
|
59
|
+
- Authentication callbacks now execute asynchronously exactly once with small, credential-free context hashes instead of request, result, or model objects. Update callback consumers and ensure the application has a durable Active Job backend where delivery matters.
|
|
60
|
+
- Production authentication now requires HTTPS by default and fails closed when a request appears insecure. Verify TLS termination and trusted proxy forwarding before deployment.
|
|
61
|
+
- Scope and permission policy is enforced at creation, update, and authentication time. Malformed scopes and scopes above a configured ceiling are rejected; blank scopes deny access whenever a scope policy is enabled.
|
|
62
|
+
- Reassess every permission ceiling configured with `public: true`. The gem validates that public tokens are non-revocable and finitely scoped, but only the host application can determine whether each named permission is safe for an untrusted client.
|
|
63
|
+
- The actively security-tested matrix is Ruby 3.3, 3.4, and 4.0 with Rails 7.2, 8.0, and 8.1. The gemspec continues to permit Ruby 3.1+ for compatibility, but older runtimes are outside the documented security-support matrix.
|
|
64
|
+
- Missing-scope responses now use HTTP 403 instead of 401. Clients that branch on the previous status should update.
|
|
65
|
+
- New one-time token handoffs are encrypted and expire after ten minutes. Custom integrations must use `ApiKeys::TokenSession` rather than reading its internal session payload directly.
|
|
66
|
+
- `last_used_at` updates are debounced for one minute by default when `track_requests_count` is false. Set `stats_update_interval = 0` for per-request timestamps; enabling exact request counting necessarily enqueues a stats job for every successful authentication.
|
|
67
|
+
- Deleting an owner now removes all associated API-key rows, including non-revocable types. Direct user-level `revoke!`, `destroy`, and `destroy!` protections remain unchanged.
|
|
68
|
+
|
|
1
69
|
## [0.3.0] - 2026-02-09
|
|
2
70
|
|
|
3
71
|
- Add Stripe-style key types and environments (publishable/secret keys with test/live isolation)
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> [!TIP]
|
|
6
6
|
> **🚀 Ship your next Rails app 10x faster!** I've built **[RailsFast](https://railsfast.com/?ref=api_keys)**, a production-ready Rails boilerplate template that comes with everything you need to launch a software business in days, not weeks. Go [check it out](https://railsfast.com/?ref=api_keys)!
|
|
7
7
|
|
|
8
|
-
`api_keys` makes it simple to add secure, production-ready API key authentication to any Rails app. Generate keys, restrict scopes, auto-expire tokens, revoke tokens, gate endpoints. It also provides a self-serve dashboard for
|
|
8
|
+
`api_keys` makes it simple to add secure, production-ready API key authentication to any Rails app. Generate keys, restrict scopes, auto-expire tokens, revoke tokens, and gate endpoints. It also provides a self-serve dashboard for users to issue and manage their own API keys. Secret tokens are hashed and shown only once. Plaintext is stored only for a key type that you explicitly mark as public, non-revocable, and limited to a finite permission set.
|
|
9
9
|
|
|
10
10
|
[ 🟢 [Live interactive demo website](https://apikeys.rameerez.com) ]
|
|
11
11
|
|
|
@@ -30,6 +30,17 @@ rails db:migrate
|
|
|
30
30
|
|
|
31
31
|
And you're done!
|
|
32
32
|
|
|
33
|
+
### Upgrading an existing installation
|
|
34
|
+
|
|
35
|
+
Install the bounded bcrypt authentication lookup index before deploying the current hardening changes:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
rails generate api_keys:add_authentication_index
|
|
39
|
+
rails db:migrate
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The generated migration is idempotent and uses a concurrent PostgreSQL index where supported.
|
|
43
|
+
|
|
33
44
|
## Quick Start
|
|
34
45
|
|
|
35
46
|
Just add `has_api_keys` to your desired model. For example, if you want your `User` records to have API keys, you'd have:
|
|
@@ -101,6 +112,16 @@ class ApplicationController < ActionController::Base
|
|
|
101
112
|
end
|
|
102
113
|
```
|
|
103
114
|
|
|
115
|
+
If the mounted dashboard should inherit from a different controller, configure it in the same initializer before the engine controllers load:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
ApiKeys.configure do |config|
|
|
119
|
+
config.parent_controller = "Admin::ApplicationController"
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The setting accepts a controller class or a valid constant-name string and defaults to `::ApplicationController`.
|
|
124
|
+
|
|
104
125
|
#### Common scenarios
|
|
105
126
|
|
|
106
127
|
**Organization with user membership:**
|
|
@@ -163,6 +184,8 @@ Tweak colors and spacing by overriding CSS variables in your application's style
|
|
|
163
184
|
}
|
|
164
185
|
```
|
|
165
186
|
|
|
187
|
+
Engine pages ship an enforcing Content Security Policy. The default allows same-origin scripts, styles, and fonts, so your own stylesheet loads normally — see [Dashboard Content Security Policy](#dashboard-content-security-policy) to tighten or disable it.
|
|
188
|
+
|
|
166
189
|
#### Building Custom Integrations
|
|
167
190
|
|
|
168
191
|
If you need complete control over the UI (e.g., to match your design system with Tailwind, Bootstrap, etc.), you can build your own views and controllers while using the gem's model layer and helpers.
|
|
@@ -260,7 +283,7 @@ module Settings
|
|
|
260
283
|
end
|
|
261
284
|
|
|
262
285
|
def success
|
|
263
|
-
@token = ApiKeys::TokenSession.retrieve_once(session)
|
|
286
|
+
@token = ApiKeys::TokenSession.retrieve_once(session, api_key: @api_key)
|
|
264
287
|
redirect_to settings_api_keys_path, alert: "Token can only be shown once." and return if @token.blank?
|
|
265
288
|
end
|
|
266
289
|
|
|
@@ -407,20 +430,26 @@ Methods available on `ApiKeys::ApiKey` instances:
|
|
|
407
430
|
|
|
408
431
|
### Token Session Helper
|
|
409
432
|
|
|
410
|
-
Manages the "show token once" pattern for secret keys
|
|
433
|
+
Manages the "show token once" pattern for secret keys. The helper encrypts each handoff with an application-derived AES-256-GCM key, binds it to the created key, expires it after ten minutes, and deletes it on first retrieval. The session contains only ciphertext and the non-secret key ID—not the plaintext token—even when the host uses Rails' encrypted cookie session store.
|
|
411
434
|
|
|
412
435
|
```ruby
|
|
413
436
|
# Store token after creation
|
|
414
437
|
ApiKeys::TokenSession.store(session, @api_key)
|
|
415
438
|
|
|
416
|
-
# Retrieve and clear (
|
|
417
|
-
@token = ApiKeys::TokenSession.retrieve_once(session)
|
|
439
|
+
# Retrieve and clear, bound to the expected key (nil on mismatch or reuse)
|
|
440
|
+
@token = ApiKeys::TokenSession.retrieve_once(session, api_key: @api_key)
|
|
418
441
|
|
|
419
442
|
# With custom session key (if managing multiple token types)
|
|
420
443
|
ApiKeys::TokenSession.store(session, @api_key, key: :my_custom_key)
|
|
421
|
-
@token = ApiKeys::TokenSession.retrieve_once(
|
|
444
|
+
@token = ApiKeys::TokenSession.retrieve_once(
|
|
445
|
+
session,
|
|
446
|
+
key: :my_custom_key,
|
|
447
|
+
api_key: @api_key
|
|
448
|
+
)
|
|
422
449
|
```
|
|
423
450
|
|
|
451
|
+
Treat the session payload as private implementation detail; use `store`, `available?`, and `retrieve_once` rather than reading it directly. A failed, expired, tampered, mismatched, or reused handoff returns `nil` and fails closed.
|
|
452
|
+
|
|
424
453
|
---
|
|
425
454
|
|
|
426
455
|
### Expiration Options Helper
|
|
@@ -668,7 +697,7 @@ If you want to write your own front-end instead of using the provided dashboard,
|
|
|
668
697
|
```ruby
|
|
669
698
|
@api_key = @user.create_api_key!(
|
|
670
699
|
name: "my-key",
|
|
671
|
-
scopes:
|
|
700
|
+
scopes: %w[read write],
|
|
672
701
|
expires_at: 42.days.from_now
|
|
673
702
|
)
|
|
674
703
|
|
|
@@ -677,14 +706,14 @@ plaintext_token = @api_key.token
|
|
|
677
706
|
# => ak_123abc...
|
|
678
707
|
```
|
|
679
708
|
|
|
680
|
-
For security reasons, the
|
|
709
|
+
For security reasons, the gem does not store generated **secret** keys in the database.
|
|
681
710
|
|
|
682
|
-
|
|
711
|
+
Only a secure digest is stored (SHA256 by default), so a secret token is available as `@api_key.token` only on the newly created in-memory object. Reloading clears it. The explicit public-key mode described below is the sole plaintext-storage exception.
|
|
683
712
|
|
|
684
713
|
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:
|
|
685
714
|
|
|
686
715
|
```bash
|
|
687
|
-
curl -X GET -H "Authorization: Bearer
|
|
716
|
+
curl -X GET -H "Authorization: Bearer YOUR_API_KEY" "https://example.com/api/endpoint" # gitleaks:allow
|
|
688
717
|
```
|
|
689
718
|
|
|
690
719
|
### Listing all keys for users
|
|
@@ -814,6 +843,8 @@ You can require a specific scope for any endpoint like:
|
|
|
814
843
|
authenticate_api_key!(scope: "write")
|
|
815
844
|
```
|
|
816
845
|
|
|
846
|
+
A missing or invalid credential returns HTTP 401. A valid key that lacks the requested scope is authenticated but unauthorized, so it returns HTTP 403 with `error: "missing_scope"`.
|
|
847
|
+
|
|
817
848
|
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:
|
|
818
849
|
|
|
819
850
|
```ruby
|
|
@@ -904,7 +935,7 @@ By default, the `api_key` gem expects API keys to come *exclusively* as HTTP Aut
|
|
|
904
935
|
https://example.com/api/endpoint?api_key=ak_123abc...
|
|
905
936
|
```
|
|
906
937
|
|
|
907
|
-
|
|
938
|
+
Do not enable this in production. URLs routinely reach logs, browser history, analytics, proxies, caches, and referrer data. If a constrained development/test integration requires it, set the expected parameter name explicitly:
|
|
908
939
|
|
|
909
940
|
```ruby
|
|
910
941
|
config.query_param = "api_key"
|
|
@@ -912,7 +943,7 @@ config.query_param = "api_key"
|
|
|
912
943
|
|
|
913
944
|
### Changing the hashing function to `bcrypt` for maximum security
|
|
914
945
|
|
|
915
|
-
By default,
|
|
946
|
+
By default, `api_keys` hashes tokens using SHA256. A fast digest is appropriate here because tokens are generated from 192 bits of randomness by default (and never less than 128 bits), rather than chosen by a human. It also permits indexed, low-latency authentication.
|
|
916
947
|
|
|
917
948
|
If you need slower, password-grade hashing (e.g., for extremely sensitive tokens), you can switch to bcrypt:
|
|
918
949
|
|
|
@@ -920,47 +951,98 @@ If you need slower, password-grade hashing (e.g., for extremely sensitive tokens
|
|
|
920
951
|
config.hash_strategy = :bcrypt
|
|
921
952
|
```
|
|
922
953
|
|
|
923
|
-
Note: bcrypt is
|
|
954
|
+
Note: bcrypt is substantially slower than SHA256. For most API use cases, SHA256 is appropriate because generated tokens have at least 128 bits of cryptographic randomness.
|
|
924
955
|
|
|
925
|
-
`sha256` has
|
|
956
|
+
`sha256` has a direct digest lookup; bcrypt requires a bounded candidate lookup and an expensive comparison. Add the authentication index shown in the upgrade section before enabling bcrypt. The gem rejects total bcrypt token values over 72 bytes to prevent bcrypt's historical input truncation behavior, so keep the configured prefix plus encoded random portion within that bound.
|
|
926
957
|
|
|
927
|
-
|
|
958
|
+
Use SHA256 for ordinary high-entropy API keys unless your threat model specifically calls for a deliberately expensive verifier.
|
|
928
959
|
|
|
929
960
|
### Increase cache TTL
|
|
930
961
|
|
|
931
|
-
We cache
|
|
962
|
+
We cache only the database ID lookup hint. Every cache hit reloads the current row and cryptographically re-verifies the presented token; cached records never authorize a request by themselves.
|
|
932
963
|
|
|
933
|
-
By default,
|
|
964
|
+
By default, the hint uses a 5-second TTL. Revocation, expiration, scope changes, environment changes, and other stored authorization state are checked from the database on every request and take effect immediately for new authentication attempts.
|
|
934
965
|
|
|
935
|
-
|
|
966
|
+
You can disable the lookup hint if you prefer not to use Rails.cache:
|
|
936
967
|
|
|
937
968
|
```ruby
|
|
938
969
|
config.cache_ttl = 0.seconds # disables caching
|
|
939
970
|
```
|
|
940
971
|
|
|
941
|
-
|
|
972
|
+
Increase the TTL to reduce repeated lookup work without making cached state authoritative:
|
|
942
973
|
|
|
943
974
|
```ruby
|
|
944
|
-
config.cache_ttl = 2.minutes
|
|
975
|
+
config.cache_ttl = 2.minutes
|
|
945
976
|
```
|
|
946
977
|
|
|
947
|
-
|
|
978
|
+
### Dashboard Content Security Policy
|
|
979
|
+
|
|
980
|
+
The mounted dashboard renders credential material, so it declares its own enforcing `Content-Security-Policy` on engine pages even when your application has none. You choose how strict it is:
|
|
981
|
+
|
|
982
|
+
```ruby
|
|
983
|
+
config.dashboard_content_security_policy = :default # :default (default), :strict, or false/nil
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
| Value | Behavior |
|
|
987
|
+
|-------|----------|
|
|
988
|
+
| `:default` | Hardened, but compatible with a normal host layout. |
|
|
989
|
+
| `:strict` | Nonce-only: nothing loads unless it carries the engine's per-request nonce. |
|
|
990
|
+
| `false` / `nil` | The gem declares nothing; your application's policy applies unchanged. |
|
|
991
|
+
|
|
992
|
+
**`:default`** emits (nonce regenerated per request):
|
|
993
|
+
|
|
994
|
+
```
|
|
995
|
+
base-uri 'none'; object-src 'none'; frame-ancestors 'none'; frame-src 'none';
|
|
996
|
+
form-action 'self'; connect-src 'self'; default-src 'self';
|
|
997
|
+
script-src 'self' 'nonce-…'; style-src 'self' 'nonce-…';
|
|
998
|
+
font-src 'self' data:; img-src 'self' https: data:
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
Framing, plugins, `<base>` hijacking, and cross-origin form posts stay blocked, and the dashboard's own inline `<style>`/`<script>` blocks stay nonce-gated — but your layout's stylesheets, scripts, and self-hosted webfonts still load.
|
|
1002
|
+
|
|
1003
|
+
**`:strict`** restores the nonce-only policy: `default-src 'none'` with no source expression for scripts or styles, so the per-request nonce is the only thing that can run.
|
|
1004
|
+
|
|
1005
|
+
```ruby
|
|
1006
|
+
config.dashboard_content_security_policy = :strict
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
Only use `:strict` if the layout rendered on engine pages serves nothing un-nonced. Rails does **not** put the nonce on `stylesheet_link_tag` / `javascript_include_tag` unless the host application opts in:
|
|
1010
|
+
|
|
1011
|
+
```ruby
|
|
1012
|
+
# config/application.rb — required for :strict with a normal host layout
|
|
1013
|
+
config.content_security_policy_nonce_generator = ->(request) { SecureRandom.base64(16) }
|
|
1014
|
+
config.content_security_policy_nonce_auto = true
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
Without that, `:strict` blocks your layout's own asset tags and the dashboard renders unstyled and inert. The gem's built-in layout is fully self-contained and works under `:strict` as-is.
|
|
1018
|
+
|
|
1019
|
+
**`false` / `nil`** leaves your application's policy completely alone. If your policy is nonce-based, allow the dashboard's nonced inline `<style>` and `<script>` yourself.
|
|
1020
|
+
|
|
1021
|
+
Any directive the gem does not set is inherited from your application's policy.
|
|
948
1022
|
|
|
949
1023
|
|
|
950
1024
|
## Callbacks: analytics, logging, usage monitoring & auditing
|
|
951
1025
|
|
|
952
|
-
The
|
|
1026
|
+
The controller concern can enqueue two callbacks for each authentication attempt. To keep secrets out of job payloads, callbacks receive small serializable context hashes—not request, result, or model objects.
|
|
953
1027
|
|
|
954
1028
|
You can define logic for them:
|
|
955
1029
|
```ruby
|
|
956
|
-
config.before_authentication = ->(
|
|
1030
|
+
config.before_authentication = ->(context) do
|
|
1031
|
+
Rails.logger.info "Authenticating request: #{context[:request_uuid]}"
|
|
1032
|
+
end
|
|
957
1033
|
|
|
958
|
-
config.after_authentication = ->(
|
|
1034
|
+
config.after_authentication = ->(context) do
|
|
1035
|
+
MyAnalytics.track_auth(
|
|
1036
|
+
success: context[:success],
|
|
1037
|
+
error_code: context[:error_code],
|
|
1038
|
+
api_key_id: context[:api_key_id]
|
|
1039
|
+
)
|
|
1040
|
+
end
|
|
959
1041
|
```
|
|
960
1042
|
|
|
961
1043
|
This is especially useful if you want to build custom monitoring, usage tracking or auditing systems on top of the `api_keys` gem.
|
|
962
1044
|
|
|
963
|
-
|
|
1045
|
+
The `before_authentication` context contains `request_uuid`. The `after_authentication` context contains `success`, `error_code`, `api_key_id`, and, when scopes were requested, `required_scope_check`. Jobs are asynchronous, so “before” means it is enqueued before verification; queue execution order is not guaranteed. Configure a persistent Active Job backend and the callback queue appropriate for your application.
|
|
964
1046
|
|
|
965
1047
|
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.
|
|
966
1048
|
|
|
@@ -972,6 +1054,15 @@ There's also a `track_requests_count` config option that you can turn on so the
|
|
|
972
1054
|
|
|
973
1055
|
But again, this is turned off by default for performance purposes, and depends on having a working, well-configured Active Job backend.
|
|
974
1056
|
|
|
1057
|
+
When exact request counting is off, `last_used_at` updates are debounced for one minute by default to avoid one queue insert and database write for every high-volume API call:
|
|
1058
|
+
|
|
1059
|
+
```ruby
|
|
1060
|
+
config.stats_update_interval = 5.minutes # coarser, lower write volume
|
|
1061
|
+
config.stats_update_interval = 0 # record every successful request
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
Enabling `track_requests_count` bypasses this debounce because every successful request must be counted. Set `enable_async_operations = false` if the application handles statistics and callbacks elsewhere and wants the gem to enqueue no background jobs.
|
|
1065
|
+
|
|
975
1066
|
## Key Types: Stripe-style Publishable & Secret Keys
|
|
976
1067
|
|
|
977
1068
|
For applications that distribute software with embedded API keys (desktop apps, mobile apps, CLI tools), you may want to differentiate between key types with different permission levels. The `api_keys` gem supports Stripe-style publishable/secret key types with optional test/live environment isolation.
|
|
@@ -980,9 +1071,9 @@ For applications that distribute software with embedded API keys (desktop apps,
|
|
|
980
1071
|
|
|
981
1072
|
When you distribute software with an embedded API key, that key can potentially be extracted by malicious users. Key types solve this by letting you create:
|
|
982
1073
|
|
|
983
|
-
- **Publishable keys** (`pk_test_...`, `pk_live_...`):
|
|
1074
|
+
- **Publishable keys** (`pk_test_...`, `pk_live_...`): Intentionally exposed identifiers. Embed them only when every configured permission is safe for an untrusted public client; assume anyone can extract and abuse them. They cannot be revoked individually.
|
|
984
1075
|
|
|
985
|
-
- **Secret keys** (`sk_test_...`, `sk_live_...`):
|
|
1076
|
+
- **Secret keys** (`sk_test_...`, `sk_live_...`): Sensitive server-side credentials whose exact access depends on their scopes. They can be revoked anytime.
|
|
986
1077
|
|
|
987
1078
|
### Configuration
|
|
988
1079
|
|
|
@@ -1015,6 +1106,10 @@ ApiKeys.configure do |config|
|
|
|
1015
1106
|
|
|
1016
1107
|
# Enable strict environment isolation (test keys fail in prod, live keys fail in dev)
|
|
1017
1108
|
config.strict_environment_isolation = true
|
|
1109
|
+
|
|
1110
|
+
# Optional: use this type when create_api_key! omits key_type.
|
|
1111
|
+
# Without a default, every new key must specify key_type explicitly.
|
|
1112
|
+
config.default_key_type = :secret
|
|
1018
1113
|
end
|
|
1019
1114
|
```
|
|
1020
1115
|
|
|
@@ -1029,7 +1124,7 @@ pk = user.create_api_key!(
|
|
|
1029
1124
|
)
|
|
1030
1125
|
pk.token # => "pk_live_abc123..."
|
|
1031
1126
|
|
|
1032
|
-
# Create a secret key (
|
|
1127
|
+
# Create a secret key (access is controlled by its scopes/type ceiling)
|
|
1033
1128
|
sk = user.create_api_key!(
|
|
1034
1129
|
name: "Admin Dashboard",
|
|
1035
1130
|
key_type: :secret
|
|
@@ -1070,6 +1165,7 @@ pk.destroy! # Raises ApiKeys::Errors::KeyNotRevocableError
|
|
|
1070
1165
|
```
|
|
1071
1166
|
|
|
1072
1167
|
The dashboard UI automatically hides the revoke button for non-revocable keys.
|
|
1168
|
+
Deleting the owning record still cascades deletion to all of its API keys, including non-revocable types, so account deletion and privacy-erasure flows cannot be blocked. The non-revocable guard applies to direct key-level user actions, not owner lifecycle cleanup.
|
|
1073
1169
|
|
|
1074
1170
|
### Public Keys (Viewable Tokens)
|
|
1075
1171
|
|
|
@@ -1081,7 +1177,7 @@ This is especially problematic when combined with `limit: 1`, which restricts us
|
|
|
1081
1177
|
|
|
1082
1178
|
#### The Solution: Storing Public Keys
|
|
1083
1179
|
|
|
1084
|
-
For publishable keys
|
|
1180
|
+
For publishable keys that grant *only operations safe for an unauthenticated public client*, hiding the token provides no secrecy benefit: distributed clients necessarily expose it. Never use this design for a permission that protects confidential data or sensitive actions.
|
|
1085
1181
|
|
|
1086
1182
|
The `public: true` option stores the plaintext token in metadata so users can view it again:
|
|
1087
1183
|
|
|
@@ -1102,23 +1198,24 @@ config.key_types = {
|
|
|
1102
1198
|
}
|
|
1103
1199
|
```
|
|
1104
1200
|
|
|
1105
|
-
#### Security
|
|
1201
|
+
#### Security constraints
|
|
1106
1202
|
|
|
1107
1203
|
> [!IMPORTANT]
|
|
1108
|
-
> The `public` option only works when
|
|
1204
|
+
> The `public` option only works when all of these conditions are met:
|
|
1109
1205
|
> - `public: true` is set in the key type configuration
|
|
1110
1206
|
> - `revocable: false` is set (non-revocable keys only)
|
|
1207
|
+
> - `permissions` is a finite, non-empty array (never `:all`)
|
|
1111
1208
|
|
|
1112
|
-
|
|
1209
|
+
These checks are deliberate safety measures:
|
|
1113
1210
|
|
|
1114
|
-
1. **
|
|
1211
|
+
1. **Configuration is validated early** — Public types must explicitly be non-revocable and have a finite, non-empty permission ceiling.
|
|
1115
1212
|
|
|
1116
1213
|
2. **Revocable keys are NEVER stored** — If a key can be revoked, users can always delete it and create a new one. There's no lockout risk, so no need to store the token.
|
|
1117
1214
|
|
|
1118
|
-
3. **
|
|
1215
|
+
3. **Your application defines what is public** — The gem cannot infer the business impact of a permission name. Only mark a type public when every permission in its ceiling is safe for an unauthenticated client to possess.
|
|
1119
1216
|
|
|
1120
1217
|
> [!WARNING]
|
|
1121
|
-
> ⚠️ **Never set `public: true` on secret keys or any key type with sensitive permissions.**
|
|
1218
|
+
> ⚠️ **Never set `public: true` on secret keys or any key type with sensitive permissions.** Validation prevents `:all` and missing permission ceilings, but your application remains responsible for classifying each named permission correctly.
|
|
1122
1219
|
|
|
1123
1220
|
When a key is public, the dashboard shows a "Show" button to reveal the full token:
|
|
1124
1221
|
|
|
@@ -1143,6 +1240,10 @@ With `strict_environment_isolation = true`, keys can only authenticate in their
|
|
|
1143
1240
|
|
|
1144
1241
|
This prevents accidentally using test keys in production (or vice versa).
|
|
1145
1242
|
|
|
1243
|
+
Typed keys always require a non-blank stored environment. When `environments` is configured, authentication also rejects typed keys whose stored environment is no longer configured, even if strict isolation is disabled. Untyped legacy keys remain compatible.
|
|
1244
|
+
|
|
1245
|
+
Once `key_types` is configured, all newly created keys must supply `key_type:` or use `default_key_type`; the gem will not silently create a new untyped key outside the configured permission and environment policy. Existing untyped keys created before enabling the feature continue to authenticate under the documented legacy rules.
|
|
1246
|
+
|
|
1146
1247
|
### Key Limits
|
|
1147
1248
|
|
|
1148
1249
|
The `limit` option restricts how many keys of a type can exist per owner per environment:
|
|
@@ -1181,7 +1282,7 @@ Existing keys without `key_type`/`environment` continue to work normally (backwa
|
|
|
1181
1282
|
## Enterprise-ready by design
|
|
1182
1283
|
The `api_keys` gem ships with:
|
|
1183
1284
|
|
|
1184
|
-
-
|
|
1285
|
+
- Active Record storage across supported Rails databases
|
|
1185
1286
|
- Async hooks
|
|
1186
1287
|
- ActiveJob support
|
|
1187
1288
|
- Polymorphic ownership (User, Org, etc.)
|
|
@@ -1203,11 +1304,17 @@ There's a demo Rails app showcasing the features in the `api_keys` gem under `te
|
|
|
1203
1304
|
|
|
1204
1305
|
## Testing
|
|
1205
1306
|
|
|
1206
|
-
Run the test suite with `bundle exec rake test
|
|
1307
|
+
Run the default test suite with `bundle exec rake test`. Run all supported Rails appraisals with:
|
|
1308
|
+
|
|
1309
|
+
```bash
|
|
1310
|
+
bundle exec appraisal rails-7.2 rake test
|
|
1311
|
+
bundle exec appraisal rails-8.0 rake test
|
|
1312
|
+
bundle exec appraisal rails-8.1 rake test
|
|
1313
|
+
```
|
|
1207
1314
|
|
|
1208
1315
|
## Development
|
|
1209
1316
|
|
|
1210
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then
|
|
1317
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then run `bundle exec rake test`. You can also run `bin/console` for an interactive prompt.
|
|
1211
1318
|
|
|
1212
1319
|
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
1213
1320
|
|
|
@@ -1215,6 +1322,8 @@ To install this gem onto your local machine, run `bundle exec rake install`.
|
|
|
1215
1322
|
|
|
1216
1323
|
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.
|
|
1217
1324
|
|
|
1325
|
+
Please report vulnerabilities privately as described in [SECURITY.md](SECURITY.md), not in a public issue.
|
|
1326
|
+
|
|
1218
1327
|
## License
|
|
1219
1328
|
|
|
1220
1329
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
`api_keys` handles authentication credentials. Please report suspected vulnerabilities privately and avoid including real API keys, production data, or customer information in any report.
|
|
4
|
+
|
|
5
|
+
## Supported versions
|
|
6
|
+
|
|
7
|
+
Security fixes are released for the latest published version. The maintained test matrix covers Ruby 3.3, 3.4, and 4.0 with patched Rails 7.2, 8.0, and 8.1 releases. Older Ruby and Rails versions may remain installable for compatibility, but runtimes that no longer receive upstream security fixes are not security-supported.
|
|
8
|
+
|
|
9
|
+
## Reporting a vulnerability
|
|
10
|
+
|
|
11
|
+
Use GitHub's **Report a vulnerability** button on the [`api_keys` security advisories page](https://github.com/rameerez/api_keys/security/advisories) so the report and any proposed fix remain private. If GitHub's private reporting flow is unavailable, email `rubygems@rameerez.com` with the subject `api_keys security report`.
|
|
12
|
+
|
|
13
|
+
Include:
|
|
14
|
+
|
|
15
|
+
- the affected version and environment;
|
|
16
|
+
- a minimal reproduction or proof of concept;
|
|
17
|
+
- the impact you believe is possible; and
|
|
18
|
+
- any suggested mitigation or patch.
|
|
19
|
+
|
|
20
|
+
Do not open a public issue for an undisclosed vulnerability. We will acknowledge the report, investigate it, and coordinate disclosure and credit with you. If the issue affects downstream applications, we will prioritize a patched release and clear upgrade guidance.
|
|
21
|
+
|
|
22
|
+
## Operational security
|
|
23
|
+
|
|
24
|
+
Applications remain responsible for:
|
|
25
|
+
|
|
26
|
+
- trusted TLS/proxy configuration, endpoint authorization, and request rate limiting;
|
|
27
|
+
- encrypted, `Secure`, `HttpOnly`, appropriately `SameSite` session cookies for the one-time dashboard token handoff;
|
|
28
|
+
- database, cache, queue, log, backup, and observability access controls;
|
|
29
|
+
- filtering any query-parameter credential name from application and proxy logs if that opt-in transport is enabled;
|
|
30
|
+
- classifying every permission on a `public: true` key type as safe for an untrusted public client and maintaining a disable/replacement procedure for non-revocable identifiers; and
|
|
31
|
+
- prompt dependency/runtime patching and a durable background-job backend where callbacks or usage statistics are required.
|
|
32
|
+
|
|
33
|
+
The gem bounds authentication work, but applications should still rate-limit credential endpoints. Do not treat a key prefix, last four characters, cache entry, or public key as proof of authorization.
|