activeadmin-device_authorization
RFC 8628-style device authorization for ActiveAdmin: public device/token endpoints, signed-in consent in the admin UI, and short-lived bearer access tokens.
Requirements
- Ruby 3.4+
- Rails 7.2.3.2+
- ActiveAdmin 3.2+
Install
From RubyGems after a release:
# Gemfile
gem "activeadmin-device_authorization"From git:
# Gemfile
gem "activeadmin-device_authorization", github: "amkisko/activeadmin-device_authorization.rb"Maintainers publish with make release (usr/bin/release.rb), which builds, pushes to RubyGems, tags the version, and creates a GitHub release.
Why these settings are configurable
-
challenge_expires_in(default 600s): RFC 8628 requiresexpires_infor device/user codes. The RFC example is 1800s; Doorkeeper device grant defaults to 300s. Always required; device codes must expire. -
poll_interval(default 5s): RFC 8628interval; clients must use 5 when omitted. -
slow_down_increment(default 5s): RFC 8628slow_downrequires increasing the interval by 5s. Keep 5 unless you have a documented reason. -
access_token_absolute_ttl(default 4h): Absolute bearer cap (RFC 6819 short-lived access tokens). Set tonilto disable absolute expiry. -
access_token_idle_ttl(default 1h): Idle timeout after last successful use (common IdP pattern with an absolute max). Set tonilto disable idle expiry. -
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. - Both access TTLs
nil: revoke-only. Allowed for low-risk hosts; not recommended for admin APIs that can read or act on customer data. -
user_code_*: RFC 8628 end-user code shape (default 8 characters in groups of 4; ambiguous I/O omitted). -
grant_retention_period/token_retention_period: Bounded cleanup of unused challenges and expired or revoked tokens. - Model class names: host keeps its own ActiveRecord models; the gem drives protocol against them.
- Scope lists, labels, and audit callbacks: product-specific; stay in the host.
- Rate-limit procs: host wires abuse limits; both are required before routes can be drawn.
Setup
# config/initializers/active_admin_device_authorization.rb
ActiveAdmin::DeviceAuthorization.configure do |config|
config.resource_owner_class_name = "AdminUser"
config.client_class_name = "DeviceAuthorizationClient"
config.grant_class_name = "DeviceAuthorizationGrant"
config.access_token_class_name = "DeviceAuthorizationAccessToken"
config.current_resource_owner = ->(controller) { controller.current_admin_user }
config.scopes = %w[api:read api:write]
config.default_scopes = %w[api:read api:write]
config.scope_labels = {
"api:read" => "API read",
"api:write" => "API write"
}
# Optional: disable idle expiry
# config.access_token_idle_ttl = nil
# Optional: never expire until revoke (not recommended for admin APIs)
# config.access_token_absolute_ttl = nil
# config.access_token_idle_ttl = nil
config.on_consent_approved = ->(resource_owner:, grant:) { }
config.on_token_issued = ->(resource_owner:, token:) { }
# These host methods must return exactly true to admit the request. They may
# render their own response and return anything else to stop processing.
config.rate_limit_device_authorization = lambda { |controller|
DeviceAuthorizationRateLimits.allow_challenge?(controller.request)
}
config.rate_limit_token = lambda { |controller|
DeviceAuthorizationRateLimits.allow_poll?(controller.request)
}
end# config/routes.rb (inside the admin namespace)
namespace :admin do
ActiveAdmin::DeviceAuthorization::Routes.draw(self)
# Optional: Routes.draw(self, consent: :controller) serves the code-entry and
# consent pages from the gem instead of ActiveAdmin page actions.
endOAuth 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#...).
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).
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.
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.
Host responsibilities
The gem does not replace host security controls. Before production traffic:
- Wire both
rate_limit_device_authorizationandrate_limit_token. Route mounting fails unless both are callable. Each request continues only when its limiter returns exactlytrue. Load-test the policies you choose. - Set
verification_base_urlto the host's trusted HTTPS origin in production. When it is unset, verification links userequest.base_url, so the host must enforce HTTPS and reject untrustedHostheaders. - Schedule
bundle exec rake active_admin:device_authorization:retentionat the cadence required by the host retention policy. Token retention deletes rows pasttoken_retention_periodafter 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). - Implement host models and indexes that match the contract below, including uniqueness and foreign keys.
- Authenticate bearer tokens in the host API (see Bearer validation contract). The gem issues and expires tokens; it does not install API authentication middleware.
Host model contract
- Client:
enabledscope,find_by!(client_id:),allows_scopes?(scopes) - Grant:
create!,exists?(user_code:),lock.find_by(...),pending?,mark_expired_if_needed!,requested_scopes/granted_scopes,last_poll_at, optionalabsolute_ttl_seconds, optionalpoll_interval_seconds(raised after eachslow_down),has_oneaccess token - Access token:
create!with digest, scopes,absolute_expires_at,last_used_at; optionalrevoked_at - Associations named from configured class names (
DeviceAuthorizationClient->device_authorization_client, and so on) - Grant states
pending,approved,denied, andexpired; onlypendingmay transition to a terminal state, and an approved grant may have at most one access token - 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
- Indexes that support expiry, revoke, idle-use, and challenge-retention queries
Bearer validation contract
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.
Register the ActiveAdmin page after configuration:
ActiveAdmin::DeviceAuthorization::Page.register!Protocol
-
POST /admin/oauth/device_authorizationwithclient_idandscope - Open
verification_uri/verification_uri_completewhile signed in as an admin user - Approve or deny (optional host
before_approve/on_approve_step_upfor step-up second factor before Approve; Deny is ungated) - Poll
POST /admin/oauth/tokenwithgrant_type=urn:ietf:params:oauth:grant-type:device_code
Development
bundle install
bundle exec appraisal install
make lint
make testMatrixed Rails versions use Appraisal: gemfiles/rails72.gemfile, rails8ruby34.gemfile, and rails8ruby4.gemfile. Run bundle exec appraisal rspec to execute RSpec in each gemfile context.
Trunk config lives in .trunk/; CI can run trunk via .github/workflows/_trunk_check.yml. Releases: make release or usr/bin/release.rb.
Security
Report vulnerabilities to security@kiskolabs.com. See SECURITY.md.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/amkisko/activeadmin-device_authorization.rb
License
The gem is available as open source under the terms of the MIT License.