activeadmin-device_authorization 0.1.3
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 +25 -0
- data/CODE_OF_CONDUCT.md +26 -0
- data/CONTRIBUTING.md +23 -0
- data/GOVERNANCE.md +25 -0
- data/LICENSE.md +21 -0
- data/README.md +162 -0
- data/SECURITY.md +25 -0
- data/activeadmin-device_authorization.gemspec +62 -0
- data/app/controllers/active_admin/device_authorization/application_controller.rb +9 -0
- data/app/controllers/active_admin/device_authorization/consents_controller.rb +83 -0
- data/app/controllers/active_admin/device_authorization/device_authorizations_controller.rb +51 -0
- data/app/controllers/active_admin/device_authorization/json_response.rb +25 -0
- data/app/controllers/active_admin/device_authorization/tokens_controller.rb +39 -0
- data/app/views/active_admin/device_authorization/_consent.html.erb +111 -0
- data/app/views/active_admin/device_authorization/_enter_code.html.erb +12 -0
- data/app/views/active_admin/device_authorization/_standalone_styles.html.erb +147 -0
- data/app/views/active_admin/device_authorization/consents/index.html.erb +19 -0
- data/app/views/active_admin/device_authorization/consents/show.html.erb +14 -0
- data/config/locales/en.yml +29 -0
- data/config/locales/fi.yml +29 -0
- data/config/locales/sv.yml +29 -0
- data/config/polyrun_coverage.yml +8 -0
- data/lib/active_admin/device_authorization/access_token_expiry.rb +46 -0
- data/lib/active_admin/device_authorization/callback.rb +15 -0
- data/lib/active_admin/device_authorization/configuration.rb +229 -0
- data/lib/active_admin/device_authorization/consent.rb +65 -0
- data/lib/active_admin/device_authorization/device_authorization_request.rb +70 -0
- data/lib/active_admin/device_authorization/engine.rb +17 -0
- data/lib/active_admin/device_authorization/page.rb +126 -0
- data/lib/active_admin/device_authorization/retention.rb +39 -0
- data/lib/active_admin/device_authorization/routes.rb +60 -0
- data/lib/active_admin/device_authorization/token_exchange.rb +144 -0
- data/lib/active_admin/device_authorization/user_code.rb +48 -0
- data/lib/active_admin/device_authorization/version.rb +7 -0
- data/lib/active_admin/device_authorization.rb +41 -0
- data/lib/activeadmin-device_authorization.rb +3 -0
- data/lib/tasks/active_admin_device_authorization.rake +10 -0
- data/sig/active_admin/device_authorization.rbs +5 -0
- metadata +296 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 17955a07d8a7c82ff64a20e93c37a448417e69dad7581fb3c80bd087db93210f
|
|
4
|
+
data.tar.gz: 5d6a98485620a0501dc4325c5c40b7e7eb2c895d8793dfe6504e91ee937b07db
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8c6a2334c39150906f51ff4cd2ac98c595752c0b318051a2d3823fb83f3b8b27d32d1a641294451440acba61a4b0892bbce07de62447bc1d959d9db55ac9aa42
|
|
7
|
+
data.tar.gz: ea90ab0aa9708c0eb4f9232782ac28f688cd1e9608ecef79e5207ed77550e53ebe98ceca5c9e51bb9c37ab321ebb25c2e548ff026825c8f7cdf4837165fab58a
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.1.3 (2026-10-26)
|
|
6
|
+
|
|
7
|
+
- Add RFC 8628 device authorization with ActiveAdmin consent, token exchange, and retention helpers
|
|
8
|
+
- Add English, Finnish, and Swedish strings for the consent and code-entry UI
|
|
9
|
+
- Add a schedulable retention task for expired authorization records
|
|
10
|
+
- Require Rails 7.2.3.2 or newer
|
|
11
|
+
- Require host rate limits before mounting the public OAuth endpoints and fail closed unless a limiter returns true
|
|
12
|
+
- Reject unknown and client-disallowed scopes with `invalid_scope`
|
|
13
|
+
- Return protocol-correct OAuth errors and cache-prevention headers
|
|
14
|
+
- Protect standalone consent decisions from cross-site request forgery
|
|
15
|
+
- Filter `device_code`, `user_code`, and `access_token` from Rails request logs
|
|
16
|
+
- Serve standalone code-entry and consent pages, normalize entered user codes, and prompt code comparison with the device
|
|
17
|
+
- Run consent and token callbacks after commit without hiding committed results when a callback fails
|
|
18
|
+
- Draw absolute controller paths so nesting under `namespace :admin` does not prefix `Admin::` onto gem controllers
|
|
19
|
+
- Clamp consent absolute TTL against `access_token_absolute_ttl`
|
|
20
|
+
- Delete idle-expired tokens during retention
|
|
21
|
+
- Raise the required poll interval by `slow_down_increment` when the grant stores `poll_interval_seconds`
|
|
22
|
+
- Document public install from RubyGems or git, host rate-limit / HTTPS / Host / retention duties, and `make release`
|
|
23
|
+
- Improve standalone consent markup for narrow screens, contrast on the code-comparison prompt, and readable user codes
|
|
24
|
+
- Require Ruby 3.4 or newer (Rails 7.2 pulls `rdoc` 8 / `rbs` 4, which need Ruby 3.3+)
|
|
25
|
+
- Wire Polyrun coverage collection and CI coverage reporting so `coverage/merged.json` is produced
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Pledge
|
|
4
|
+
|
|
5
|
+
Participation is a harassment-free experience. Treat each other with respect. Keep private context private.
|
|
6
|
+
|
|
7
|
+
## Expected behavior
|
|
8
|
+
|
|
9
|
+
- Engage with genuine curiosity. Admit uncertainty.
|
|
10
|
+
- Own every line you submit, including generated code.
|
|
11
|
+
- Correct errors politely.
|
|
12
|
+
- Use language that welcomes people and respects identity and privacy.
|
|
13
|
+
|
|
14
|
+
## Unacceptable behavior
|
|
15
|
+
|
|
16
|
+
- Harassment, trolling, or insulting comments, public or private.
|
|
17
|
+
- Using jargon or volume, including automated spam, to silence others.
|
|
18
|
+
- Sharing others' data or personal context without permission.
|
|
19
|
+
|
|
20
|
+
## Automation
|
|
21
|
+
|
|
22
|
+
You remain responsible for bugs, security holes, and bias in generated artifacts you submit.
|
|
23
|
+
|
|
24
|
+
## Enforcement
|
|
25
|
+
|
|
26
|
+
Report abusive or harassing behavior to contact@kiskolabs.com. Reports are reviewed and investigated.
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Contributing Guidelines
|
|
2
|
+
|
|
3
|
+
## Automation authorship
|
|
4
|
+
|
|
5
|
+
You may use AI and automation tools. You remain the author of every line you submit: review, debug, and understand the change.
|
|
6
|
+
|
|
7
|
+
Keep pull requests small enough to review. Annotate non-obvious logic.
|
|
8
|
+
|
|
9
|
+
Never send project secrets or private context to public models.
|
|
10
|
+
|
|
11
|
+
## How to contribute
|
|
12
|
+
|
|
13
|
+
### Reporting issues
|
|
14
|
+
|
|
15
|
+
Verify the facts before filing. Use issue templates for goals, constraints, and reproduction steps.
|
|
16
|
+
|
|
17
|
+
### Pull requests
|
|
18
|
+
|
|
19
|
+
Keep one pull request to one goal. Explain why the change is needed. Run smoke tests and regression checks locally before opening the request.
|
|
20
|
+
|
|
21
|
+
### Review
|
|
22
|
+
|
|
23
|
+
Reviewers will ask about approach and trade-offs. Treat suggested changes as part of the work.
|
data/GOVERNANCE.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Project Governance
|
|
2
|
+
|
|
3
|
+
## Roles
|
|
4
|
+
|
|
5
|
+
### Contributors
|
|
6
|
+
|
|
7
|
+
Anyone who submits code, documentation, or participates in discussions. Contributors take ownership of problems they identify and follow the project's quality standards.
|
|
8
|
+
|
|
9
|
+
### Maintainers
|
|
10
|
+
|
|
11
|
+
Maintainers:
|
|
12
|
+
|
|
13
|
+
1. Set scope and architectural direction.
|
|
14
|
+
2. Review contributions (human or automated) against security and logic standards.
|
|
15
|
+
3. Decide when the community is divided.
|
|
16
|
+
|
|
17
|
+
## Decision making
|
|
18
|
+
|
|
19
|
+
### Lazy consensus
|
|
20
|
+
|
|
21
|
+
For routine changes, a proposal that draws no objection within two weeks is approved.
|
|
22
|
+
|
|
23
|
+
### Maintainer approval
|
|
24
|
+
|
|
25
|
+
Major architectural changes, high-risk automation integrations, or changes that affect business logic need explicit maintainer approval. A change must solve a real problem.
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 amkisko
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# activeadmin-device_authorization
|
|
2
|
+
|
|
3
|
+
[](https://github.com/amkisko/activeadmin-device_authorization.rb/actions/workflows/test.yml)
|
|
4
|
+
|
|
5
|
+
RFC 8628-style device authorization for ActiveAdmin: public device/token endpoints, signed-in consent in the admin UI, and short-lived bearer access tokens.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Ruby 3.4+
|
|
10
|
+
- Rails 7.2.3.2+
|
|
11
|
+
- ActiveAdmin 3.2+
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
From RubyGems after a release:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
# Gemfile
|
|
19
|
+
gem "activeadmin-device_authorization"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
From git:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
# Gemfile
|
|
26
|
+
gem "activeadmin-device_authorization", github: "amkisko/activeadmin-device_authorization.rb"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Maintainers publish with `make release` (`usr/bin/release.rb`), which builds, pushes to RubyGems, tags the version, and creates a GitHub release.
|
|
30
|
+
|
|
31
|
+
## Why these settings are configurable
|
|
32
|
+
|
|
33
|
+
- `challenge_expires_in` (default 600s): RFC 8628 requires `expires_in` for device/user codes. The RFC example is 1800s; Doorkeeper device grant defaults to 300s. Always required; device codes must expire.
|
|
34
|
+
- `poll_interval` (default 5s): RFC 8628 `interval`; clients must use 5 when omitted.
|
|
35
|
+
- `slow_down_increment` (default 5s): RFC 8628 `slow_down` requires increasing the interval by 5s. Keep 5 unless you have a documented reason.
|
|
36
|
+
- `access_token_absolute_ttl` (default 4h): Absolute bearer cap (RFC 6819 short-lived access tokens). Set to `nil` to disable absolute expiry.
|
|
37
|
+
- `access_token_idle_ttl` (default 1h): Idle timeout after last successful use (common IdP pattern with an absolute max). Set to `nil` to disable idle expiry.
|
|
38
|
+
- `access_token_default_absolute_ttl` / min / max / `choices`: Consent UI lifetime picker and clamp so a client cannot ask for longer than the host allows.
|
|
39
|
+
- Both access TTLs `nil`: revoke-only. Allowed for low-risk hosts; not recommended for admin APIs that can read or act on customer data.
|
|
40
|
+
- `user_code_*`: RFC 8628 end-user code shape (default 8 characters in groups of 4; ambiguous I/O omitted).
|
|
41
|
+
- `grant_retention_period` / `token_retention_period`: Bounded cleanup of unused challenges and expired or revoked tokens.
|
|
42
|
+
- Model class names: host keeps its own ActiveRecord models; the gem drives protocol against them.
|
|
43
|
+
- Scope lists, labels, and audit callbacks: product-specific; stay in the host.
|
|
44
|
+
- Rate-limit procs: host wires abuse limits; both are required before routes can be drawn.
|
|
45
|
+
|
|
46
|
+
## Setup
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# config/initializers/active_admin_device_authorization.rb
|
|
50
|
+
ActiveAdmin::DeviceAuthorization.configure do |config|
|
|
51
|
+
config.resource_owner_class_name = "AdminUser"
|
|
52
|
+
config.client_class_name = "DeviceAuthorizationClient"
|
|
53
|
+
config.grant_class_name = "DeviceAuthorizationGrant"
|
|
54
|
+
config.access_token_class_name = "DeviceAuthorizationAccessToken"
|
|
55
|
+
config.current_resource_owner = ->(controller) { controller.current_admin_user }
|
|
56
|
+
|
|
57
|
+
config.scopes = %w[api:read api:write]
|
|
58
|
+
config.default_scopes = %w[api:read api:write]
|
|
59
|
+
config.scope_labels = {
|
|
60
|
+
"api:read" => "API read",
|
|
61
|
+
"api:write" => "API write"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Optional: disable idle expiry
|
|
65
|
+
# config.access_token_idle_ttl = nil
|
|
66
|
+
|
|
67
|
+
# Optional: never expire until revoke (not recommended for admin APIs)
|
|
68
|
+
# config.access_token_absolute_ttl = nil
|
|
69
|
+
# config.access_token_idle_ttl = nil
|
|
70
|
+
|
|
71
|
+
config.on_consent_approved = ->(resource_owner:, grant:) { }
|
|
72
|
+
config.on_token_issued = ->(resource_owner:, token:) { }
|
|
73
|
+
|
|
74
|
+
# These host methods must return exactly true to admit the request. They may
|
|
75
|
+
# render their own response and return anything else to stop processing.
|
|
76
|
+
config.rate_limit_device_authorization = lambda { |controller|
|
|
77
|
+
DeviceAuthorizationRateLimits.allow_challenge?(controller.request)
|
|
78
|
+
}
|
|
79
|
+
config.rate_limit_token = lambda { |controller|
|
|
80
|
+
DeviceAuthorizationRateLimits.allow_poll?(controller.request)
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# config/routes.rb (inside the admin namespace)
|
|
87
|
+
namespace :admin do
|
|
88
|
+
ActiveAdmin::DeviceAuthorization::Routes.draw(self)
|
|
89
|
+
# Optional: Routes.draw(self, consent: :controller) serves the code-entry and
|
|
90
|
+
# consent pages from the gem instead of ActiveAdmin page actions.
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
OAuth controller paths are absolute (`/active_admin/device_authorization/...`) so nesting under `namespace :admin` does not invent `Admin::ActiveAdmin::...` constants. Consent defaults to the ActiveAdmin page (`device_auth#...`).
|
|
95
|
+
|
|
96
|
+
`access_token_absolute_ttl` is the hard ceiling. Consent `min` / `max` / `default` / `choices` must sit inside that ceiling; `clamp_absolute_ttl` uses `min(access_token_max_absolute_ttl, access_token_absolute_ttl)`.
|
|
97
|
+
|
|
98
|
+
Consent and token callbacks run after their database commit. A callback exception is reported through `Rails.error` and does not hide the committed decision or issued token from the caller. Use a durable host-side job or outbox when delivery must be retried.
|
|
99
|
+
|
|
100
|
+
RFC 8628 `slow_down` raises the required poll interval by `slow_down_increment` when the grant stores `poll_interval_seconds`. Without that column, the server still returns `slow_down` against the base `poll_interval` only.
|
|
101
|
+
|
|
102
|
+
### Host responsibilities
|
|
103
|
+
|
|
104
|
+
The gem does not replace host security controls. Before production traffic:
|
|
105
|
+
|
|
106
|
+
- Wire both `rate_limit_device_authorization` and `rate_limit_token`. Route mounting fails unless both are callable. Each request continues only when its limiter returns exactly `true`. Load-test the policies you choose.
|
|
107
|
+
- Set `verification_base_url` to the host's trusted HTTPS origin in production. When it is unset, verification links use `request.base_url`, so the host must enforce HTTPS and reject untrusted `Host` headers.
|
|
108
|
+
- Schedule `bundle exec rake active_admin:device_authorization:retention` at the cadence required by the host retention policy. Token retention deletes rows past `token_retention_period` after absolute expiry, revoke, or idle expiry (when idle TTL is enabled). Do not rely on absolute expiry alone when absolute TTL is nil (far-future sentinel).
|
|
109
|
+
- Implement host models and indexes that match the contract below, including uniqueness and foreign keys.
|
|
110
|
+
- Authenticate bearer tokens in the host API (see Bearer validation contract). The gem issues and expires tokens; it does not install API authentication middleware.
|
|
111
|
+
|
|
112
|
+
### Host model contract
|
|
113
|
+
|
|
114
|
+
- Client: `enabled` scope, `find_by!(client_id:)`, `allows_scopes?(scopes)`
|
|
115
|
+
- Grant: `create!`, `exists?(user_code:)`, `lock.find_by(...)`, `pending?`, `mark_expired_if_needed!`, `requested_scopes` / `granted_scopes`, `last_poll_at`, optional `absolute_ttl_seconds`, optional `poll_interval_seconds` (raised after each `slow_down`), `has_one` access token
|
|
116
|
+
- Access token: `create!` with digest, scopes, `absolute_expires_at`, `last_used_at`; optional `revoked_at`
|
|
117
|
+
- Associations named from configured class names (`DeviceAuthorizationClient` -> `device_authorization_client`, and so on)
|
|
118
|
+
- Grant states `pending`, `approved`, `denied`, and `expired`; only `pending` may transition to a terminal state, and an approved grant may have at most one access token
|
|
119
|
+
- Unique indexes for client IDs, user codes, token digests, each client/device-code-digest pair, and the access token's grant foreign key; foreign keys for the grant, client, and resource owner associations
|
|
120
|
+
- Indexes that support expiry, revoke, idle-use, and challenge-retention queries
|
|
121
|
+
|
|
122
|
+
### Bearer validation contract
|
|
123
|
+
|
|
124
|
+
The gem issues and expires tokens, but it does not install authentication in the host API. For every protected request, the host must digest the presented bearer value with `ActiveAdmin::DeviceAuthorization.config.digest`, find the matching token through the allowed client or resource-owner relation, reject it when `AccessTokenExpiry#expired?` is true, enforce its scopes, and update `last_used_at` only after successful authentication. A missing, revoked, expired, or out-of-scope token must fail closed.
|
|
125
|
+
|
|
126
|
+
Register the ActiveAdmin page after configuration:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
ActiveAdmin::DeviceAuthorization::Page.register!
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Protocol
|
|
133
|
+
|
|
134
|
+
1. `POST /admin/oauth/device_authorization` with `client_id` and `scope`
|
|
135
|
+
2. Open `verification_uri` / `verification_uri_complete` while signed in as an admin user
|
|
136
|
+
3. Approve or deny (optional host `before_approve` / `on_approve_step_up` for step-up second factor before Approve; Deny is ungated)
|
|
137
|
+
4. Poll `POST /admin/oauth/token` with `grant_type=urn:ietf:params:oauth:grant-type:device_code`
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
bundle install
|
|
143
|
+
bundle exec appraisal install
|
|
144
|
+
make lint
|
|
145
|
+
make test
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Matrixed Rails versions use [Appraisal](https://github.com/thoughtbot/appraisal): `gemfiles/rails72.gemfile`, `rails8ruby34.gemfile`, and `rails8ruby4.gemfile`. Run `bundle exec appraisal rspec` to execute RSpec in each gemfile context.
|
|
149
|
+
|
|
150
|
+
[Trunk](https://docs.trunk.io) config lives in `.trunk/`; CI can run `trunk` via `.github/workflows/_trunk_check.yml`. Releases: `make release` or `usr/bin/release.rb`.
|
|
151
|
+
|
|
152
|
+
## Security
|
|
153
|
+
|
|
154
|
+
Report vulnerabilities to security@kiskolabs.com. See [SECURITY.md](SECURITY.md).
|
|
155
|
+
|
|
156
|
+
## Contributing
|
|
157
|
+
|
|
158
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/amkisko/activeadmin-device_authorization.rb
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE.md).
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# SECURITY
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
Email security details to: security@kiskolabs.com
|
|
6
|
+
|
|
7
|
+
Send the report only to that address. Include description, steps to reproduce, potential impact, and a suggested fix when you have one.
|
|
8
|
+
|
|
9
|
+
### Response Timeline
|
|
10
|
+
|
|
11
|
+
- We will acknowledge receipt of your report.
|
|
12
|
+
- We will provide an initial assessment.
|
|
13
|
+
- We will keep you informed of our progress and resolution timeline.
|
|
14
|
+
|
|
15
|
+
### Disclosure Policy
|
|
16
|
+
|
|
17
|
+
- We will work with you to understand and resolve the issue.
|
|
18
|
+
- We will credit you for the discovery unless you prefer to remain anonymous.
|
|
19
|
+
- We will publish a security advisory after the vulnerability is patched.
|
|
20
|
+
- We will coordinate public disclosure with you.
|
|
21
|
+
|
|
22
|
+
## Automation Security
|
|
23
|
+
|
|
24
|
+
- Keep production credentials, API keys, and personal data out of prompts sent to third-party models or automation services.
|
|
25
|
+
- Verify automated dependencies.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/active_admin/device_authorization/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "activeadmin-device_authorization"
|
|
7
|
+
spec.version = ActiveAdmin::DeviceAuthorization::VERSION
|
|
8
|
+
spec.authors = ["Andrei Makarov"]
|
|
9
|
+
spec.email = ["contact@kiskolabs.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "RFC 8628 device authorization for ActiveAdmin bearer access"
|
|
12
|
+
spec.description = "Configurable device authorization grant, ActiveAdmin consent UI, and short-lived bearer tokens for admin APIs."
|
|
13
|
+
spec.license = "MIT"
|
|
14
|
+
spec.platform = Gem::Platform::RUBY
|
|
15
|
+
spec.required_ruby_version = ">= 3.4.0"
|
|
16
|
+
|
|
17
|
+
repository_url = "https://github.com/amkisko/activeadmin-device_authorization.rb"
|
|
18
|
+
|
|
19
|
+
spec.homepage = repository_url
|
|
20
|
+
spec.metadata = {
|
|
21
|
+
"homepage_uri" => repository_url,
|
|
22
|
+
"source_code_uri" => "#{repository_url}/tree/main",
|
|
23
|
+
"changelog_uri" => "#{repository_url}/blob/main/CHANGELOG.md",
|
|
24
|
+
"bug_tracker_uri" => "#{repository_url}/issues",
|
|
25
|
+
"documentation_uri" => "#{repository_url}#readme",
|
|
26
|
+
"allowed_push_host" => "https://rubygems.org",
|
|
27
|
+
"rubygems_mfa_required" => "true"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
spec.files = Dir.chdir(__dir__) do
|
|
31
|
+
%w[
|
|
32
|
+
CHANGELOG.md
|
|
33
|
+
CODE_OF_CONDUCT.md
|
|
34
|
+
CONTRIBUTING.md
|
|
35
|
+
GOVERNANCE.md
|
|
36
|
+
LICENSE.md
|
|
37
|
+
README.md
|
|
38
|
+
SECURITY.md
|
|
39
|
+
activeadmin-device_authorization.gemspec
|
|
40
|
+
].select { |path| File.file?(path) } +
|
|
41
|
+
Dir["{app,config,lib,sig}/**/*"]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
spec.require_paths = ["lib"]
|
|
45
|
+
|
|
46
|
+
spec.add_dependency "activeadmin", ">= 3.2"
|
|
47
|
+
spec.add_dependency "rails", ">= 7.2.3.2"
|
|
48
|
+
|
|
49
|
+
spec.add_development_dependency "appraisal", "~> 2"
|
|
50
|
+
spec.add_development_dependency "bundler", ">= 2"
|
|
51
|
+
spec.add_development_dependency "bundler-audit", "~> 0.9"
|
|
52
|
+
spec.add_development_dependency "polyrun", ">= 2.2.0"
|
|
53
|
+
spec.add_development_dependency "rake", "~> 13"
|
|
54
|
+
spec.add_development_dependency "rbs", "~> 4"
|
|
55
|
+
spec.add_development_dependency "rspec", "~> 3"
|
|
56
|
+
spec.add_development_dependency "rubocop-rspec", "~> 3.8"
|
|
57
|
+
spec.add_development_dependency "rubocop-thread_safety", "~> 0.7"
|
|
58
|
+
spec.add_development_dependency "standard", "~> 1.52"
|
|
59
|
+
spec.add_development_dependency "standard-custom", "~> 1.0"
|
|
60
|
+
spec.add_development_dependency "standard-performance", "~> 1.8"
|
|
61
|
+
spec.add_development_dependency "standard-rspec", "~> 0.3"
|
|
62
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module DeviceAuthorization
|
|
5
|
+
# Thin HTML consent actions when the host does not use the ActiveAdmin page.
|
|
6
|
+
# Prefer ActiveAdmin::DeviceAuthorization::Page.register! for the AA layout.
|
|
7
|
+
class ConsentsController < ApplicationController
|
|
8
|
+
before_action :authenticate_resource_owner!
|
|
9
|
+
before_action :load_grant!, except: :index
|
|
10
|
+
|
|
11
|
+
def index
|
|
12
|
+
user_code = UserCode.normalize(params[:user_code])
|
|
13
|
+
if user_code.present?
|
|
14
|
+
redirect_to send(
|
|
15
|
+
:"#{ActiveAdmin::DeviceAuthorization.config.path_namespace}_device_auth_grant_path",
|
|
16
|
+
user_code
|
|
17
|
+
)
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
render layout: false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def show
|
|
25
|
+
@grant.mark_expired_if_needed!
|
|
26
|
+
render layout: false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def approve
|
|
30
|
+
config = ActiveAdmin::DeviceAuthorization.config
|
|
31
|
+
if config.before_approve && !config.before_approve.call(self)
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Consent.new(
|
|
36
|
+
grant: @grant,
|
|
37
|
+
resource_owner: current_resource_owner,
|
|
38
|
+
scopes: Array(params[:scopes]),
|
|
39
|
+
absolute_ttl_seconds: params[:absolute_ttl_seconds]
|
|
40
|
+
).approve!
|
|
41
|
+
redirect_to after_consent_path, notice: I18n.t("active_admin.device_authorization.approved")
|
|
42
|
+
rescue Consent::Error => error
|
|
43
|
+
redirect_to after_consent_path,
|
|
44
|
+
alert: I18n.t("active_admin.device_authorization.error.#{error.message}", default: error.message)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def deny
|
|
48
|
+
Consent.new(
|
|
49
|
+
grant: @grant,
|
|
50
|
+
resource_owner: current_resource_owner,
|
|
51
|
+
scopes: @grant.requested_scopes,
|
|
52
|
+
absolute_ttl_seconds: ActiveAdmin::DeviceAuthorization.config.access_token_default_absolute_ttl
|
|
53
|
+
).deny!
|
|
54
|
+
redirect_to after_consent_path, notice: I18n.t("active_admin.device_authorization.denied")
|
|
55
|
+
rescue Consent::Error => error
|
|
56
|
+
redirect_to after_consent_path,
|
|
57
|
+
alert: I18n.t("active_admin.device_authorization.error.#{error.message}", default: error.message)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def authenticate_resource_owner!
|
|
63
|
+
return if current_resource_owner
|
|
64
|
+
|
|
65
|
+
head :unauthorized
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def current_resource_owner
|
|
69
|
+
@current_resource_owner ||= ActiveAdmin::DeviceAuthorization.config.current_resource_owner.call(self)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def load_grant!
|
|
73
|
+
@grant = ActiveAdmin::DeviceAuthorization.config.grant_class
|
|
74
|
+
.find_by!(user_code: UserCode.normalize(params[:user_code]))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def after_consent_path
|
|
78
|
+
config = ActiveAdmin::DeviceAuthorization.config
|
|
79
|
+
"/#{config.path_namespace}/#{config.verification_path}/#{@grant.user_code}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module DeviceAuthorization
|
|
5
|
+
class DeviceAuthorizationsController < ApplicationController
|
|
6
|
+
include JsonResponse
|
|
7
|
+
|
|
8
|
+
skip_forgery_protection
|
|
9
|
+
before_action :enforce_rate_limit
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
return if performed?
|
|
13
|
+
|
|
14
|
+
config = ActiveAdmin::DeviceAuthorization.config
|
|
15
|
+
base_url = config.verification_base_url.presence || request.base_url
|
|
16
|
+
result = DeviceAuthorizationRequest.new(
|
|
17
|
+
client_id: params.require(:client_id),
|
|
18
|
+
scopes: params[:scope].to_s.split(/\s+/),
|
|
19
|
+
verification_base_url: base_url,
|
|
20
|
+
config: config
|
|
21
|
+
).call
|
|
22
|
+
|
|
23
|
+
render_oauth_json(
|
|
24
|
+
{
|
|
25
|
+
device_code: result.device_code,
|
|
26
|
+
user_code: result.user_code,
|
|
27
|
+
verification_uri: result.verification_uri,
|
|
28
|
+
verification_uri_complete: result.verification_uri_complete,
|
|
29
|
+
expires_in: result.expires_in,
|
|
30
|
+
interval: result.interval
|
|
31
|
+
},
|
|
32
|
+
status: :ok
|
|
33
|
+
)
|
|
34
|
+
rescue ActiveRecord::RecordNotFound
|
|
35
|
+
render_oauth_json({error: "invalid_client"}, status: :bad_request)
|
|
36
|
+
rescue DeviceAuthorizationRequest::Error => error
|
|
37
|
+
render_oauth_json({error: error.message}, status: :bad_request)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def enforce_rate_limit
|
|
43
|
+
limiter = ActiveAdmin::DeviceAuthorization.config.rate_limit_device_authorization
|
|
44
|
+
allowed = limiter.respond_to?(:call) && limiter.call(self) == true
|
|
45
|
+
return if performed? || allowed
|
|
46
|
+
|
|
47
|
+
render_oauth_json({error: "slow_down"}, status: :too_many_requests)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module DeviceAuthorization
|
|
5
|
+
module JsonResponse
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
rescue_from ActionController::ParameterMissing, with: :respond_with_invalid_request
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def respond_with_invalid_request(_error)
|
|
15
|
+
render_oauth_json({error: "invalid_request"}, status: :bad_request)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def render_oauth_json(payload, status:)
|
|
19
|
+
response.set_header("Cache-Control", "no-store")
|
|
20
|
+
response.set_header("Pragma", "no-cache")
|
|
21
|
+
render json: payload, status: status
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module DeviceAuthorization
|
|
5
|
+
class TokensController < ApplicationController
|
|
6
|
+
include JsonResponse
|
|
7
|
+
|
|
8
|
+
skip_forgery_protection
|
|
9
|
+
before_action :enforce_rate_limit
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
return if performed?
|
|
13
|
+
|
|
14
|
+
unless params[:grant_type].to_s == TokenExchange::GRANT_TYPE
|
|
15
|
+
return render_oauth_json({error: "unsupported_grant_type"}, status: :bad_request)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
payload = TokenExchange.new(
|
|
19
|
+
client_id: params.require(:client_id),
|
|
20
|
+
device_code: params.require(:device_code)
|
|
21
|
+
).call
|
|
22
|
+
|
|
23
|
+
render_oauth_json(payload, status: :ok)
|
|
24
|
+
rescue TokenExchange::Error => error
|
|
25
|
+
render_oauth_json({error: error.code}, status: error.http_status)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def enforce_rate_limit
|
|
31
|
+
limiter = ActiveAdmin::DeviceAuthorization.config.rate_limit_token
|
|
32
|
+
allowed = limiter.respond_to?(:call) && limiter.call(self) == true
|
|
33
|
+
return if performed? || allowed
|
|
34
|
+
|
|
35
|
+
render_oauth_json({error: "slow_down"}, status: :too_many_requests)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|