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
@@ -0,0 +1,106 @@
1
+ <%# Shared form for creating and editing API Keys %>
2
+ <%= form_with(model: [:keys, api_key], url: (api_key.persisted? ? key_path(api_key) : keys_path), local: true) do |form| %>
3
+ <% if api_key.errors.any? %>
4
+ <div style="color: red;">
5
+ <strong><%= pluralize(api_key.errors.count, "error") %> prohibited this API key from being saved:</strong>
6
+ <ul>
7
+ <% api_key.errors.full_messages.each do |message| %>
8
+ <li><%= message %></li>
9
+ <% end %>
10
+ </ul>
11
+ </div>
12
+ <% end %>
13
+
14
+ <%# Fields only available on NEW %>
15
+ <% unless api_key.persisted? %>
16
+ <div>
17
+ <%= form.label :name, "Name (optional)" %>
18
+ <%= form.text_field :name, placeholder: "e.g., myproject-production-key" %>
19
+ </div>
20
+
21
+ <div>
22
+ <%= form.label :expires_at_preset, "Expiration" %>
23
+ <%= form.select :expires_at_preset,
24
+ options_for_select([
25
+ ["No Expiration", "no_expiration"],
26
+ ["7 days", "7_days"],
27
+ ["30 days", "30_days"],
28
+ ["60 days", "60_days"],
29
+ ["90 days", "90_days"],
30
+ ["365 days", "365_days"] # Common presets
31
+ ], api_key.expires_at.present? ? nil : "no_expiration"), # Default selection
32
+ {}, # html options
33
+ {} # data attributes
34
+ %>
35
+ </div>
36
+
37
+ <%# This assumes a variable `@available_scopes` is passed to the view, %>
38
+ <%# containing an array of scope strings allowed for this context. %>
39
+ <%# This list might come from global configuration or owner-specific settings. %>
40
+ <%# Example: @available_scopes = ["read", "write", "admin"] %>
41
+ <% @available_scopes = current_api_keys_user.class.api_keys_settings.dig(:default_scopes) %>
42
+ <% if defined?(@available_scopes) && @available_scopes.present? %>
43
+ <div>
44
+ <%= form.label :scopes, "Permissions" %>
45
+ <% @available_scopes.each do |scope| %>
46
+ <div class="form-check">
47
+ <%= form.check_box :scopes,
48
+ { multiple: true, # Submits as an array
49
+ class: "form-check-input",
50
+ checked: true,
51
+ id: "api_key_scopes_#{scope.parameterize}" }, # Unique ID
52
+ scope, # Value submitted when checked
53
+ nil # Value submitted when unchecked (not needed here)
54
+ %>
55
+ <%= form.label "scopes_#{scope.parameterize}", scope.humanize, class: "form-check-label" %>
56
+ </div>
57
+ <% end %>
58
+ </div>
59
+ <% end %>
60
+
61
+ <% end %>
62
+
63
+ <%# Fields editable on EDIT %>
64
+ <% if api_key.persisted? %>
65
+ <div>
66
+ <%= form.label :name, "Key Name (optional)" %>
67
+ <%= form.text_field :name %>
68
+ </div>
69
+
70
+ <%# Define available scopes for the edit form context %>
71
+ <%# This assumes the available scopes are the same as the default ones. %>
72
+ <%# Consider passing @available_scopes from the controller if logic is more complex. %>
73
+ <% @available_scopes = current_api_keys_user.class.api_keys_settings.dig(:default_scopes) %>
74
+ <% if defined?(@available_scopes) && @available_scopes.present? %>
75
+ <div>
76
+ <%= form.label :scopes, "Permissions / Scopes" %>
77
+ <% @available_scopes.each do |scope| %>
78
+ <div class="form-check">
79
+ <%= form.check_box :scopes,
80
+ { multiple: true, # Submits as an array
81
+ class: "form-check-input",
82
+ checked: api_key.scopes&.include?(scope), # Check if scope is already assigned
83
+ id: "api_key_scopes_#{scope.parameterize}_edit" }, # Unique ID for edit form
84
+ scope, # Value submitted when checked
85
+ nil # Value submitted when unchecked (not needed here)
86
+ %>
87
+ <%# Using the scope name directly for the label's `for` attribute requires matching the checkbox ID %>
88
+ <%= form.label "scopes_#{scope.parameterize}_edit", scope.humanize, class: "form-check-label" %>
89
+ </div>
90
+ <% end %>
91
+ </div>
92
+ <% end %>
93
+ <% end %>
94
+
95
+ <div>
96
+ <% if api_key.persisted? %>
97
+ <%= form.submit "Update Key" %>
98
+ <%= link_to "Cancel", keys_path %>
99
+ <% else %>
100
+ <h4><strong>Keep it safe</strong></h4>
101
+ <p>Your API key will only be shown once after creation. <strong>Your key cannot be recovered:</strong> copy it immediately and store it securely.</p>
102
+ <%= form.submit "Create API Key" %>
103
+ <%= link_to "Cancel", keys_path %>
104
+ <% end %>
105
+ </div>
106
+ <% end %>
@@ -0,0 +1,72 @@
1
+ <tr>
2
+ <td>
3
+ <%# Status indicator (no text originally, keeping it that way unless specified otherwise) %>
4
+ <% if key.active? %>
5
+ <span style="color: green;"></span>
6
+ <% elsif key.revoked? %>
7
+ <span style="color: orange;">[Revoked]</span>
8
+ <% elsif key.expired? %>
9
+ <span style="color: red;">[Expired]</span>
10
+ <% end %>
11
+ <%= key.name.presence || "Secret key" %>
12
+ </td>
13
+ <td><code><%= key.masked_token %></code></td>
14
+
15
+
16
+ <td title="<%= key.created_at.strftime('%Y-%m-%d %H:%M:%S %Z') %>">
17
+ <%= time_ago_in_words(key.created_at) %> ago
18
+ </td>
19
+
20
+
21
+ <td>
22
+ <% if key.expires_at? %>
23
+ <% if key.expired? %>
24
+ <strong style="color: red;" title="<%= key.expires_at.strftime('%Y-%m-%d %H:%M:%S %Z') %>">
25
+ Expired <%= time_ago_in_words(key.expires_at) %> ago
26
+ </strong>
27
+ <% else %>
28
+ <span title="<%= key.expires_at.strftime('%Y-%m-%d %H:%M:%S %Z') %>">
29
+ Expires in <%= time_ago_in_words(key.expires_at) %>
30
+ </span>
31
+ <% end %>
32
+ <% else %>
33
+ <em>Never expires</em>
34
+ <% end %>
35
+ </td>
36
+
37
+
38
+ <td>
39
+ <% if key.last_used_at? %>
40
+ <span title="<%= key.last_used_at.strftime('%Y-%m-%d %H:%M:%S %Z') %>">
41
+ <%= time_ago_in_words(key.last_used_at) %> ago
42
+ </span>
43
+ <%# TODO: Add relative time check (e.g., "within last 3 months") %>
44
+ <% else %>
45
+ <em>Never used</em>
46
+ <% end %>
47
+ </td>
48
+
49
+
50
+ <td>
51
+ <% if key.scopes.present? %>
52
+ <% key.scopes.each do |scope| %>
53
+ <kbd class="tag is-small"><%= scope %></kbd>
54
+ <% end %>
55
+ <% else %>
56
+ &mdash;
57
+ <% end %>
58
+ </td>
59
+ <td class="api-keys-action-buttons">
60
+ <% if key.active? %>
61
+ <%= link_to api_keys.edit_key_path(key), title: "Edit Key" do %>
62
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M16.793 2.793a3.121 3.121 0 1 1 4.414 4.414l-8.5 8.5A1 1 0 0 1 12 16H9a1 1 0 0 1-1-1v-3a1 1 0 0 1 .293-.707l8.5-8.5Zm3 1.414a1.121 1.121 0 0 0-1.586 0L10 12.414V14h1.586l8.207-8.207a1.121 1.121 0 0 0 0-1.586ZM6 5a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-4a1 1 0 1 1 2 0v4a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H6Z" clip-rule="evenodd"></path></svg>
63
+ <% end %>
64
+ <%= button_to api_keys.revoke_key_path(key), title: "Revoke Key", data: { turbo_method: :post, turbo_confirm: "Are you sure you want to revoke this key? It will stop working immediately." } do %>
65
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M10.556 4a1 1 0 0 0-.97.751l-.292 1.14h5.421l-.293-1.14A1 1 0 0 0 13.453 4h-2.897Zm6.224 1.892-.421-1.639A3 3 0 0 0 13.453 2h-2.897A3 3 0 0 0 7.65 4.253l-.421 1.639H4a1 1 0 1 0 0 2h.1l1.215 11.425A3 3 0 0 0 8.3 22h7.4a3 3 0 0 0 2.984-2.683l1.214-11.425H20a1 1 0 1 0 0-2h-3.22Zm1.108 2H6.112l1.192 11.214A1 1 0 0 0 8.3 20h7.4a1 1 0 0 0 .995-.894l1.192-11.214ZM10 10a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z" clip-rule="evenodd"></path></svg>
66
+ <% end %>
67
+ <% else %>
68
+ <%# No actions available for inactive/revoked/expired keys %>
69
+ &mdash;
70
+ <% end %>
71
+ </td>
72
+ </tr>
@@ -0,0 +1,52 @@
1
+ <%# Partial for displaying the API keys table %>
2
+ <%# Locals: active_keys (Active keys collection), inactive_keys (Inactive keys collection) %>
3
+
4
+ <%# Combine active and inactive keys for a unified table view %>
5
+ <% all_keys = active_keys + inactive_keys %>
6
+
7
+ <section aria-labelledby="api-keys-heading">
8
+
9
+ <div class="api-keys-table-wrapper">
10
+ <% if all_keys.any? %>
11
+ <table>
12
+ <thead>
13
+ <tr>
14
+ <th>Name</th>
15
+ <th>Secret Key</th>
16
+ <th>Created</th>
17
+ <th>Expires</th>
18
+ <th>Last Used</th>
19
+ <th>Permissions</th>
20
+ <th>Actions</th>
21
+ </tr>
22
+ </thead>
23
+ <tbody>
24
+ <%# Render active keys first %>
25
+ <% active_keys.each do |key| %>
26
+ <%= render partial: 'api_keys/keys/key_row', locals: { key: key } %>
27
+ <% end %>
28
+
29
+ <%# Render inactive keys below, visually distinct %>
30
+ <% if inactive_keys.any? %>
31
+ <%# Optional: Add a visual separator if desired %>
32
+ <%# <tr><td colspan="7" style="border-top: 2px solid #ccc; text-align: center; padding-top: 1em; color: #555;">Inactive Keys</td></tr> %>
33
+
34
+ <% inactive_keys.each do |key| %>
35
+ <%= render partial: 'api_keys/keys/key_row', locals: { key: key, inactive: true } %>
36
+ <% end %>
37
+ <% end %>
38
+ </tbody>
39
+ </table>
40
+ <% else %>
41
+ <div style="text-align: center; padding: 2em;">
42
+ <h4>You don't have any API keys yet!</h4>
43
+ <p>Create your first API key to get started.</p>
44
+ <%# Consider adding a primary "Create Key" button here %>
45
+ <%#= link_to "Create New API Key", new_key_path, class: "button primary" %>
46
+ </div>
47
+ <% end %>
48
+ </div>
49
+
50
+ </section>
51
+
52
+ <%# The separate inactive keys section is now removed as they are integrated above. %>
@@ -0,0 +1,88 @@
1
+ <%# Partial for displaying the newly created API key token %>
2
+ <%# Locals: api_key (ApiKey instance), plaintext_token (String) %>
3
+
4
+ <h2>Save your key</h2>
5
+
6
+ <p>Please save your secret key in a safe place since <strong>you won't be able to view it again</strong>. Keep it secure, as anyone with your API key can make requests on your behalf. If you do lose it, you'll need to generate a new one.</p>
7
+
8
+ <p>
9
+ <%= link_to api_keys.security_best_practices_path, class: "text-primary api-keys-align-center" do %>
10
+ Learn more about API key best practices&nbsp;
11
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M15 5a1 1 0 1 1 0-2h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-5.293 5.293a1 1 0 0 1-1.414-1.414L17.586 5H15ZM4 7a3 3 0 0 1 3-3h3a1 1 0 1 1 0 2H7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-3a1 1 0 1 1 2 0v3a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7Z" clip-rule="evenodd"></path></svg>
12
+ <% end %>
13
+ </p>
14
+
15
+
16
+ <div style="padding: 1em; margin: 1em 0; border-radius: 4px;">
17
+ <div class="card bd-primary">
18
+ <div class="row">
19
+ <div class="col-7 is-vertical-align is-center">
20
+ <pre id="api-key-token" style="word-wrap: break-word;"><%= plaintext_token %></pre>
21
+ </div>
22
+
23
+ <div class="col is-vertical-align is-center">
24
+ <button id="copy-api-key-button" onclick="copyTokenToClipboard()" class="button primary api-keys-align-center">
25
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M7 5a3 3 0 0 1 3-3h9a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-2v2a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3v-9a3 3 0 0 1 3-3h2V5Zm2 2h5a3 3 0 0 1 3 3v5h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-9a1 1 0 0 0-1 1v2ZM5 9a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1v-9a1 1 0 0 0-1-1H5Z" clip-rule="evenodd"></path></svg>
26
+ <span class="api-keys-button-text">Copy</span>
27
+ </button>
28
+ </div>
29
+
30
+ </div>
31
+ </div>
32
+
33
+
34
+ <% if api_key.scopes.present? %>
35
+ <div style="margin-top: 2em;">
36
+ <p><strong>Permissions</strong></p>
37
+
38
+ <% if api_key.scopes.present? %>
39
+ <% api_key.scopes.each do |scope| %>
40
+ <kbd class="tag is-small"><%= scope %></kbd>
41
+ <% end %>
42
+ <% else %>
43
+ &mdash;
44
+ <% end %>
45
+ </div>
46
+ <% end %>
47
+ </div>
48
+
49
+ <%# Simple JavaScript for copy-to-clipboard functionality %>
50
+ <%# Ensure this script is loaded only once if rendering multiple components %>
51
+ <script>
52
+ function copyTokenToClipboard() {
53
+ const tokenElement = document.getElementById('api-key-token');
54
+ const copyButton = document.getElementById('copy-api-key-button');
55
+ const buttonTextElement = copyButton.querySelector('.api-keys-button-text'); // Target the span containing the text
56
+ const originalButtonText = buttonTextElement.innerHTML; // Store original HTML
57
+
58
+ if (navigator.clipboard && tokenElement && copyButton && buttonTextElement) {
59
+ navigator.clipboard.writeText(tokenElement.textContent || '').then(() => {
60
+ buttonTextElement.innerHTML = 'Copied!'; // Update text
61
+ copyButton.classList.add('success'); // Optional: Add class for styling
62
+ setTimeout(() => {
63
+ buttonTextElement.innerHTML = originalButtonText; // Revert text
64
+ copyButton.classList.remove('success'); // Optional: Remove class
65
+ }, 2000);
66
+ }).catch(err => {
67
+ buttonTextElement.innerHTML = 'Failed'; // Update text on error
68
+ copyButton.classList.add('error'); // Optional: Add class for styling
69
+ console.error('Failed to copy text: ', err);
70
+ setTimeout(() => {
71
+ buttonTextElement.innerHTML = originalButtonText; // Revert text
72
+ copyButton.classList.remove('error'); // Optional: Remove class
73
+ }, 2000);
74
+ });
75
+ } else {
76
+ // Fallback or indicate unavailability more clearly if needed
77
+ buttonTextElement.innerHTML = 'Cannot Copy';
78
+ copyButton.classList.add('error'); // Optional: Add class for styling
79
+ console.warn('Clipboard API not available or element missing.');
80
+ setTimeout(() => {
81
+ buttonTextElement.innerHTML = originalButtonText; // Revert text
82
+ copyButton.classList.remove('error'); // Optional: Remove class
83
+ }, 2000);
84
+ }
85
+ }
86
+ // Automatically try to copy when the partial is rendered, if desired?
87
+ // document.addEventListener('DOMContentLoaded', copyTokenToClipboard); // Example
88
+ </script>
@@ -0,0 +1,5 @@
1
+ <div class="col-5">
2
+ <h2>Edit API Key: <%= @api_key.name.presence || @api_key.masked_token %></h2>
3
+
4
+ <%= render 'form', api_key: @api_key %>
5
+ </div>
@@ -0,0 +1,26 @@
1
+ <header>
2
+ <div class="row">
3
+ <div class="col">
4
+ <h1>API Keys</h1>
5
+ </div>
6
+
7
+ <div class="col api-keys-align-center is-right">
8
+ <%= link_to new_key_path, class: "button primary api-keys-align-center", role: "button" do %>
9
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 5a1 1 0 0 1 1 1v5h5a1 1 0 1 1 0 2h-5v5a1 1 0 1 1-2 0v-5H6a1 1 0 1 1 0-2h5V6a1 1 0 0 1 1-1Z" clip-rule="evenodd"></path></svg>
10
+ <span>&nbsp;Create new secret key</span>
11
+ <% end %>
12
+ </div>
13
+
14
+ </header>
15
+
16
+ <div>
17
+
18
+ <p>Do not share your API key with others or expose it in the browser or other client-side code. <%= link_to api_keys.security_best_practices_path, class: "text-primary api-keys-align-center" do %>
19
+ Learn more&nbsp;
20
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M15 5a1 1 0 1 1 0-2h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-5.293 5.293a1 1 0 0 1-1.414-1.414L17.586 5H15ZM4 7a3 3 0 0 1 3-3h3a1 1 0 1 1 0 2H7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-3a1 1 0 1 1 2 0v3a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7Z" clip-rule="evenodd"></path></svg>
21
+ <% end %>
22
+
23
+ <%# Render the reusable table partial %>
24
+ <%= render partial: 'keys_table', locals: { active_keys: @api_keys, inactive_keys: @inactive_api_keys } %>
25
+
26
+ </div>
@@ -0,0 +1,5 @@
1
+ <div class="col-5">
2
+ <h2>Create New API Key</h2>
3
+
4
+ <%= render 'form', api_key: @api_key %>
5
+ </div>
@@ -0,0 +1,12 @@
1
+ <div class="col-5">
2
+
3
+ <%# Renders the block showing the newly created token %>
4
+ <%= render partial: 'show_token', locals: { api_key: @api_key, plaintext_token: @plaintext_token } %>
5
+
6
+ <%# Add navigation links %>
7
+ <p style="margin-top: 2em;">
8
+ <%= link_to ApiKeys.configuration.return_text, ApiKeys.configuration.return_url, class: "button outline" %>
9
+ <%= link_to "All API Keys", keys_path, class: "button" %>
10
+ </p>
11
+
12
+ </div>
@@ -0,0 +1,70 @@
1
+ <header>
2
+ <h1>API Key Security Best Practices</h1>
3
+ <p>Protecting your API keys is crucial for maintaining the security and integrity of your account and data.</p>
4
+ </header>
5
+
6
+ <article class="col-5">
7
+
8
+ <section>
9
+ <h3>1. Treat API Keys Like Passwords</h3>
10
+ <p>Your API keys grant access to your account and potentially sensitive operations. Handle them with the same level of security you would apply to your account password or other critical credentials.</p>
11
+ </section>
12
+
13
+ <section>
14
+ <h3>2. Use Unique Keys for Different Applications & Environments</h3>
15
+ <p>Generate distinct API keys for different applications, services, or integrations that need access. If a key for one application is compromised, you can revoke it without disrupting others. Use separate keys for development, staging, and production environments.</p>
16
+ <p><em>Tip:</em> Use the "Name" field when creating keys to easily identify their purpose (e.g., "Production Zapier Integration", "Staging iOS App").</p>
17
+ </section>
18
+
19
+ <section>
20
+ <h3>3. Never Expose Keys in Client-Side Code</h3>
21
+ <p><strong>Never</strong> embed API keys directly in mobile apps (iOS, Android), browser-side JavaScript, desktop applications, or any code that resides on a user's device. Exposed keys can be easily extracted by malicious actors.</p>
22
+ <p><strong>Solution:</strong> Route API requests through your own backend server. Your server can securely store and use the API key to communicate with the target API on behalf of the client.</p>
23
+ </section>
24
+
25
+ <section>
26
+ <h3>4. Never Commit Keys to Version Control (e.g., Git)</h3>
27
+ <p>Committing keys to your source code repository (like Git, Mercurial, etc.) is a common and dangerous mistake. Even in private repositories, accidental pushes or repository breaches can leak your keys.</p>
28
+ <p><strong>Solution:</strong> Store keys in environment variables or use a dedicated secrets management system. Access the key in your code via these secure methods.</p>
29
+ </section>
30
+
31
+ <section>
32
+ <h3>5. Securely Store Keys on Your Backend</h3>
33
+ <ul>
34
+ <li><strong>Environment Variables:</strong> The simplest secure method for many applications. Set an environment variable (e.g., `YOUR_SERVICE_API_KEY`) on your server and access it in your code (e.g., `ENV['YOUR_SERVICE_API_KEY']` in Ruby/Rails).</li>
35
+ <li><strong>Secrets Management Services:</strong> For more robust needs, especially in production or team environments, use dedicated services like HashiCorp Vault, AWS Secrets Manager, Google Secret Manager, Doppler, etc. These provide encrypted storage, access control, auditing, and often automated rotation capabilities.</li>
36
+ <li><strong>Encrypted Configuration Files:</strong> If using configuration files, ensure they are encrypted (e.g., Rails encrypted credentials `config/credentials.yml.enc` and `Rails.application.credentials`). <%= link_to "More info here", "https://guides.rubyonrails.org/security.html#custom-credentials", target: "_blank", rel: "noopener noreferrer", class: "text-primary" %>.</li>
37
+ </ul>
38
+ </section>
39
+
40
+ <section>
41
+ <h3>6. Implement the Principle of Least Privilege (Scopes)</h3>
42
+ <p>If the API service supports it (and this `api_keys` gem allows for scopes), create keys with only the minimum permissions (scopes) required for their specific task. Avoid using a key with full access if only read access is needed.</p>
43
+ <p><em>Note:</em> Scope availability and enforcement depend on how the host application integrates and utilizes the `scopes` attribute provided by this gem.</p>
44
+ </section>
45
+
46
+ <section>
47
+ <h3>7. Monitor Usage and Rotate Keys Regularly</h3>
48
+ <ul>
49
+ <li><strong>Monitor Usage:</strong> Regularly check API usage logs or dashboards (if provided by the service or your monitoring tools). Look for unexpected spikes in activity or requests from unusual locations, which could indicate a compromised key.</li>
50
+ <li><strong>Rotate Keys:</strong> Periodically generate new keys and revoke old ones (key rotation). This limits the window of opportunity for attackers if a key is ever leaked undetected. How often you rotate depends on your security requirements (e.g., every 90 days, annually).
51
+ <br><em>Tip:</em> This dashboard allows creating multiple keys, facilitating rotation. Create a new key, update your application(s), verify they work, and then revoke the old key.</li>
52
+ <li><strong>Revoke Immediately if Compromised:</strong> If you suspect a key has been leaked or compromised, revoke it immediately using the "Revoke" button on your keys dashboard.</li>
53
+ </ul>
54
+ </section>
55
+
56
+ <section>
57
+ <h3>8. Use HTTPS Exclusively</h3>
58
+ <p>Ensure all API requests are made over HTTPS to encrypt the connection and prevent eavesdropping. Transmitting keys over unencrypted HTTP is highly insecure.</p>
59
+ </section>
60
+
61
+ <hr>
62
+
63
+ <p>By following these best practices, you significantly reduce the risk associated with API key management.</p>
64
+
65
+ <%# Link back to the keys index if appropriate %>
66
+ <% if defined?(api_keys.keys_path) %>
67
+ <p><%= link_to "Back to API Keys", api_keys.keys_path, class: "text-primary" %></p>
68
+ <% end %>
69
+
70
+ </article>
@@ -0,0 +1,115 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>🔑 API Keys</title>
7
+ <%# Note: No CSS included here to keep it CSS-framework-agnostic %>
8
+ <link rel="stylesheet" href="https://unpkg.com/chota@latest">
9
+ <%# Host application is expected to provide styling %>
10
+ <%= csrf_meta_tags %>
11
+ <%= csp_meta_tag %>
12
+ </head>
13
+ <body>
14
+ <style>
15
+ body {
16
+ /* grid-template-columns: 1fr min(100rem, 90%) 1fr !important; */
17
+ }
18
+
19
+ @media (prefers-color-scheme: dark) {
20
+ body {
21
+ /* Define dark mode variables directly */
22
+ --bg-color:rgb(14, 14, 14);
23
+ --bg-secondary-color:rgb(34, 34, 34);
24
+ --font-color: #f5f5f5;
25
+ --color-grey: #ccc;
26
+ --color-darkGrey: #777;
27
+ }
28
+ }
29
+
30
+ code, pre {
31
+ color: var(--font-color);
32
+ font-size: 0.8em;
33
+ }
34
+
35
+ .api-keys-align-center {
36
+ display: inline-flex;
37
+ align-items: center;
38
+ }
39
+
40
+ form > div {
41
+ margin-top: 2.5em;
42
+ }
43
+
44
+ a {
45
+ color: var(--font-color);
46
+ }
47
+
48
+ .api-keys-action-buttons form {
49
+ display: inline;
50
+ }
51
+
52
+ .api-keys-action-buttons button {
53
+ background: none;
54
+ padding: 0;
55
+ color: #c23539
56
+ }
57
+
58
+ .api-keys-button-text {
59
+ padding-left: 0.2em;
60
+ }
61
+
62
+ #api-keys-flash-container {
63
+ margin-top: 1em;
64
+ }
65
+
66
+ .api-keys-flash-message {
67
+ background-color: rgba(255, 255, 255, 0.15);
68
+ padding: 0.8em 0;
69
+ height: 100;
70
+ }
71
+
72
+ article h2, article li {
73
+ margin-top: 1.5em;
74
+ }
75
+
76
+ article h3 {
77
+ margin-top: 2.4em;
78
+ }
79
+
80
+ </style>
81
+
82
+ <nav class="nav container">
83
+ <div class="nav-left">
84
+ <%# Use configured return URL and text %>
85
+ <%= link_to ApiKeys.configuration.return_text, ApiKeys.configuration.return_url %>
86
+ <%# Link to keys index using engine's path helper %>
87
+ <%= link_to "My API Keys", api_keys.keys_path, class: "active" %>
88
+ </div>
89
+ <div class="nav-center">
90
+ </div>
91
+ <div class="nav-right">
92
+ </div>
93
+ </nav>
94
+
95
+ <aside id="api-keys-flash-container" class="container text-center">
96
+ <%# Flash Messages %>
97
+ <% flash.each do |type, msg| %>
98
+ <div class="api-keys-flash-message api-keys-flash-<%= type %>">
99
+ <%= msg %>
100
+ </div>
101
+ <% end %>
102
+ </aside>
103
+
104
+ <main class="container">
105
+ <%= yield %>
106
+ </main>
107
+
108
+ <footer>
109
+ <%# Footer content %>
110
+ </footer>
111
+
112
+ <%# Add any necessary JS includes if needed later %>
113
+
114
+ </body>
115
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ ApiKeys::Engine.routes.draw do
4
+ # User-facing API Key management
5
+ resources :keys, only: [:index, :new, :create, :show, :edit, :update] do
6
+ member do
7
+ post :revoke # Using POST for actions that change state
8
+ end
9
+ end
10
+
11
+ # Static pages
12
+ namespace :security do
13
+ get :best_practices
14
+ end
15
+
16
+ # Root of the engine could point to the keys index
17
+ root to: "keys#index"
18
+ end