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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +25 -0
  3. data/CODE_OF_CONDUCT.md +26 -0
  4. data/CONTRIBUTING.md +23 -0
  5. data/GOVERNANCE.md +25 -0
  6. data/LICENSE.md +21 -0
  7. data/README.md +162 -0
  8. data/SECURITY.md +25 -0
  9. data/activeadmin-device_authorization.gemspec +62 -0
  10. data/app/controllers/active_admin/device_authorization/application_controller.rb +9 -0
  11. data/app/controllers/active_admin/device_authorization/consents_controller.rb +83 -0
  12. data/app/controllers/active_admin/device_authorization/device_authorizations_controller.rb +51 -0
  13. data/app/controllers/active_admin/device_authorization/json_response.rb +25 -0
  14. data/app/controllers/active_admin/device_authorization/tokens_controller.rb +39 -0
  15. data/app/views/active_admin/device_authorization/_consent.html.erb +111 -0
  16. data/app/views/active_admin/device_authorization/_enter_code.html.erb +12 -0
  17. data/app/views/active_admin/device_authorization/_standalone_styles.html.erb +147 -0
  18. data/app/views/active_admin/device_authorization/consents/index.html.erb +19 -0
  19. data/app/views/active_admin/device_authorization/consents/show.html.erb +14 -0
  20. data/config/locales/en.yml +29 -0
  21. data/config/locales/fi.yml +29 -0
  22. data/config/locales/sv.yml +29 -0
  23. data/config/polyrun_coverage.yml +8 -0
  24. data/lib/active_admin/device_authorization/access_token_expiry.rb +46 -0
  25. data/lib/active_admin/device_authorization/callback.rb +15 -0
  26. data/lib/active_admin/device_authorization/configuration.rb +229 -0
  27. data/lib/active_admin/device_authorization/consent.rb +65 -0
  28. data/lib/active_admin/device_authorization/device_authorization_request.rb +70 -0
  29. data/lib/active_admin/device_authorization/engine.rb +17 -0
  30. data/lib/active_admin/device_authorization/page.rb +126 -0
  31. data/lib/active_admin/device_authorization/retention.rb +39 -0
  32. data/lib/active_admin/device_authorization/routes.rb +60 -0
  33. data/lib/active_admin/device_authorization/token_exchange.rb +144 -0
  34. data/lib/active_admin/device_authorization/user_code.rb +48 -0
  35. data/lib/active_admin/device_authorization/version.rb +7 -0
  36. data/lib/active_admin/device_authorization.rb +41 -0
  37. data/lib/activeadmin-device_authorization.rb +3 -0
  38. data/lib/tasks/active_admin_device_authorization.rake +10 -0
  39. data/sig/active_admin/device_authorization.rbs +5 -0
  40. 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
@@ -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
+ [![Test Status](https://github.com/amkisko/activeadmin-device_authorization.rb/actions/workflows/test.yml/badge.svg)](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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module DeviceAuthorization
5
+ class ApplicationController < ActionController::Base
6
+ protect_from_forgery with: :exception
7
+ end
8
+ end
9
+ 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