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
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module DeviceAuthorization
5
+ # Host-facing knobs for RFC 8628 device authorization and bearer access tokens.
6
+ #
7
+ # Challenge lifetime and poll interval follow RFC 8628 (expires_in required;
8
+ # interval defaults to 5 when omitted). Access-token absolute and idle TTLs
9
+ # follow common OAuth practice (short-lived bearer; idle + absolute caps as
10
+ # in major IdP refresh/access policies). Either access-token TTL may be nil
11
+ # to disable that axis; both nil means revoke-only expiry.
12
+ class Configuration
13
+ DEFAULT_USER_CODE_ALPHABET = (
14
+ ("A".."Z").to_a + ("2".."9").to_a - %w[I O]
15
+ ).freeze
16
+
17
+ attr_accessor :resource_owner_class_name,
18
+ :client_class_name,
19
+ :grant_class_name,
20
+ :access_token_class_name,
21
+ :current_resource_owner,
22
+ :challenge_expires_in,
23
+ :poll_interval,
24
+ :slow_down_increment,
25
+ :access_token_absolute_ttl,
26
+ :access_token_idle_ttl,
27
+ :access_token_default_absolute_ttl,
28
+ :access_token_min_absolute_ttl,
29
+ :access_token_max_absolute_ttl,
30
+ :access_token_absolute_ttl_choices,
31
+ :user_code_character_count,
32
+ :user_code_group_size,
33
+ :user_code_alphabet,
34
+ :verification_path,
35
+ :oauth_path_prefix,
36
+ :path_namespace,
37
+ :scopes,
38
+ :default_scopes,
39
+ :scope_labels,
40
+ :grant_retention_period,
41
+ :token_retention_period,
42
+ :retention_batch_size,
43
+ :token_digest,
44
+ :on_consent_approved,
45
+ :on_consent_denied,
46
+ :on_token_issued,
47
+ # Callable(controller) -> truthy to continue approve, falsy if the host
48
+ # already redirected (step-up, etc.).
49
+ :before_approve,
50
+ # Callable(controller) for GET/POST step-up page_action when present.
51
+ :on_approve_step_up,
52
+ :rate_limit_device_authorization,
53
+ :rate_limit_token,
54
+ :register_active_admin_page,
55
+ :menu_parent,
56
+ :menu_priority,
57
+ :menu_label,
58
+ :verification_base_url
59
+
60
+ def initialize
61
+ @resource_owner_class_name = "AdminUser"
62
+ @client_class_name = "DeviceAuthorizationClient"
63
+ @grant_class_name = "DeviceAuthorizationGrant"
64
+ @access_token_class_name = "DeviceAuthorizationAccessToken"
65
+ @current_resource_owner = ->(controller) { controller.send(:current_admin_user) }
66
+
67
+ # RFC 8628 example uses 1800s; Doorkeeper device grant defaults to 300s.
68
+ # Ten minutes is a practical admin-consent window.
69
+ @challenge_expires_in = 10 * 60
70
+ @poll_interval = 5
71
+ @slow_down_increment = 5
72
+
73
+ @access_token_absolute_ttl = 4 * 60 * 60
74
+ @access_token_idle_ttl = 60 * 60
75
+ @access_token_default_absolute_ttl = 60 * 60
76
+ @access_token_min_absolute_ttl = 60 * 60
77
+ @access_token_max_absolute_ttl = 4 * 60 * 60
78
+ @access_token_absolute_ttl_choices = [1, 2, 3, 4].map { |hours| hours * 60 * 60 }
79
+
80
+ @user_code_character_count = 8
81
+ @user_code_group_size = 4
82
+ @user_code_alphabet = DEFAULT_USER_CODE_ALPHABET
83
+
84
+ @verification_path = "device_auth"
85
+ @oauth_path_prefix = "oauth"
86
+ @path_namespace = :admin
87
+
88
+ @scopes = []
89
+ @default_scopes = []
90
+ @scope_labels = {}
91
+
92
+ @grant_retention_period = 3 * 24 * 60 * 60
93
+ @token_retention_period = 30 * 24 * 60 * 60
94
+ @retention_batch_size = 1000
95
+
96
+ @token_digest = ->(plaintext) { Digest::SHA256.hexdigest(plaintext.to_s) }
97
+
98
+ @on_consent_approved = nil
99
+ @on_consent_denied = nil
100
+ @on_token_issued = nil
101
+ @before_approve = nil
102
+ @on_approve_step_up = nil
103
+ @rate_limit_device_authorization = nil
104
+ @rate_limit_token = nil
105
+
106
+ @register_active_admin_page = true
107
+ @menu_parent = "Admin"
108
+ @menu_priority = 91
109
+ @menu_label = nil
110
+ @verification_base_url = nil
111
+ end
112
+
113
+ def resource_owner_class
114
+ resource_owner_class_name.constantize
115
+ end
116
+
117
+ def client_class
118
+ client_class_name.constantize
119
+ end
120
+
121
+ def grant_class
122
+ grant_class_name.constantize
123
+ end
124
+
125
+ def access_token_class
126
+ access_token_class_name.constantize
127
+ end
128
+
129
+ def absolute_expiry_enabled?
130
+ !access_token_absolute_ttl.nil?
131
+ end
132
+
133
+ def idle_expiry_enabled?
134
+ !access_token_idle_ttl.nil?
135
+ end
136
+
137
+ def digest(plaintext)
138
+ token_digest.call(plaintext)
139
+ end
140
+
141
+ def label_for_scope(scope)
142
+ key = scope.to_s
143
+ return scope_labels[key] if scope_labels.key?(key)
144
+ return scope_labels[key.to_sym] if scope_labels.key?(key.to_sym)
145
+
146
+ key
147
+ end
148
+
149
+ def normalize_scopes(requested)
150
+ allowed = Array(scopes).map(&:to_s)
151
+ Array(requested).flatten.compact.map(&:to_s).map(&:strip).uniq.select { |scope| allowed.include?(scope) }
152
+ end
153
+
154
+ def validate_rate_limits!
155
+ [:rate_limit_device_authorization, :rate_limit_token].each do |setting|
156
+ raise ArgumentError, "#{setting} must be callable" unless public_send(setting).respond_to?(:call)
157
+ end
158
+ end
159
+
160
+ # Hard ceiling is access_token_absolute_ttl. Consent max cannot exceed it.
161
+ def effective_max_absolute_ttl
162
+ return nil unless absolute_expiry_enabled?
163
+
164
+ [access_token_max_absolute_ttl.to_i, access_token_absolute_ttl.to_i].min
165
+ end
166
+
167
+ def clamp_absolute_ttl(seconds)
168
+ return nil unless absolute_expiry_enabled?
169
+
170
+ value = seconds.to_i
171
+ value = access_token_default_absolute_ttl.to_i if value <= 0
172
+ min = access_token_min_absolute_ttl.to_i
173
+ max = effective_max_absolute_ttl
174
+ value.clamp(min, max)
175
+ end
176
+
177
+ def validate!
178
+ if challenge_expires_in.to_i <= 0
179
+ raise ArgumentError, "challenge_expires_in must be a positive number of seconds (RFC 8628 expires_in)"
180
+ end
181
+ if poll_interval.to_i <= 0
182
+ raise ArgumentError, "poll_interval must be a positive number of seconds (RFC 8628 interval)"
183
+ end
184
+ if slow_down_increment.to_i <= 0
185
+ raise ArgumentError, "slow_down_increment must be a positive number of seconds (RFC 8628 slow_down)"
186
+ end
187
+ if absolute_expiry_enabled?
188
+ validate_absolute_ttl_window!
189
+ end
190
+ if idle_expiry_enabled? && access_token_idle_ttl.to_i <= 0
191
+ raise ArgumentError, "access_token_idle_ttl must be positive when set"
192
+ end
193
+ true
194
+ end
195
+
196
+ private
197
+
198
+ def validate_absolute_ttl_window!
199
+ abs = access_token_absolute_ttl.to_i
200
+ min = access_token_min_absolute_ttl.to_i
201
+ max = access_token_max_absolute_ttl.to_i
202
+ default = access_token_default_absolute_ttl.to_i
203
+ raise ArgumentError, "access_token_absolute_ttl must be positive when set" if abs <= 0
204
+ raise ArgumentError, "access_token_min_absolute_ttl must be positive" if min <= 0
205
+ raise ArgumentError, "access_token_max_absolute_ttl must be positive" if max <= 0
206
+ raise ArgumentError, "access_token_default_absolute_ttl must be positive" if default <= 0
207
+ if max > abs
208
+ raise ArgumentError,
209
+ "access_token_max_absolute_ttl (#{max}) cannot exceed access_token_absolute_ttl (#{abs})"
210
+ end
211
+ if min > max
212
+ raise ArgumentError,
213
+ "access_token_min_absolute_ttl (#{min}) cannot exceed access_token_max_absolute_ttl (#{max})"
214
+ end
215
+ if default < min || default > max
216
+ raise ArgumentError,
217
+ "access_token_default_absolute_ttl (#{default}) must be within min..max (#{min}..#{max})"
218
+ end
219
+ Array(access_token_absolute_ttl_choices).each do |choice|
220
+ seconds = choice.to_i
221
+ next if seconds.between?(min, max)
222
+
223
+ raise ArgumentError,
224
+ "access_token_absolute_ttl_choices entry #{seconds} must be within min..max (#{min}..#{max})"
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module DeviceAuthorization
5
+ class Consent
6
+ class Error < StandardError; end
7
+
8
+ def initialize(grant:, resource_owner:, scopes:, absolute_ttl_seconds:, config: ActiveAdmin::DeviceAuthorization.config)
9
+ @grant = grant
10
+ @resource_owner = resource_owner
11
+ @scopes = config.normalize_scopes(scopes)
12
+ @absolute_ttl_seconds = config.clamp_absolute_ttl(absolute_ttl_seconds)
13
+ @config = config
14
+ end
15
+
16
+ def approve!
17
+ @grant.with_lock do
18
+ raise Error, "not_pending" unless @grant.pending?
19
+ raise Error, "expired" if @grant.mark_expired_if_needed!
20
+ raise Error, "invalid_scope" if @scopes.empty?
21
+ raise Error, "unsupported_scope" unless client.allows_scopes?(@scopes)
22
+ raise Error, "scope_not_requested" unless (@scopes - @grant.requested_scopes).empty?
23
+ if @resource_owner.respond_to?(:active_for_authentication?) && !@resource_owner.active_for_authentication?
24
+ raise Error, "resource_owner_inactive"
25
+ end
26
+
27
+ attrs = {
28
+ status: "approved",
29
+ granted_scopes: @scopes
30
+ }
31
+ attrs[resource_owner_association] = @resource_owner
32
+ attrs[:absolute_ttl_seconds] = @absolute_ttl_seconds if @grant.respond_to?(:absolute_ttl_seconds=)
33
+ @grant.update!(attrs)
34
+ end
35
+ Callback.call(@config.on_consent_approved, resource_owner: @resource_owner, grant: @grant)
36
+ @grant
37
+ end
38
+
39
+ def deny!
40
+ @grant.with_lock do
41
+ raise Error, "not_pending" unless @grant.pending?
42
+ @grant.mark_expired_if_needed!
43
+ raise Error, "expired" if @grant.status == "expired"
44
+
45
+ attrs = {status: "denied"}
46
+ attrs[resource_owner_association] = @resource_owner
47
+ @grant.update!(attrs)
48
+ end
49
+ Callback.call(@config.on_consent_denied, resource_owner: @resource_owner, grant: @grant)
50
+ @grant
51
+ end
52
+
53
+ private
54
+
55
+ def client
56
+ association = @config.client_class_name.underscore
57
+ @grant.public_send(association)
58
+ end
59
+
60
+ def resource_owner_association
61
+ @config.resource_owner_class_name.underscore.to_sym
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module DeviceAuthorization
5
+ class DeviceAuthorizationRequest
6
+ class Error < StandardError; end
7
+
8
+ Result = Data.define(
9
+ :device_code,
10
+ :user_code,
11
+ :verification_uri,
12
+ :verification_uri_complete,
13
+ :expires_in,
14
+ :interval
15
+ )
16
+
17
+ def initialize(client_id:, scopes:, verification_base_url:, config: ActiveAdmin::DeviceAuthorization.config)
18
+ @client_id = client_id
19
+ @requested_scopes = Array(scopes).flatten.compact.map(&:to_s).map(&:strip).reject(&:empty?).uniq
20
+ @scopes = config.normalize_scopes(scopes)
21
+ @verification_base_url = verification_base_url
22
+ @config = config
23
+ end
24
+
25
+ def call
26
+ client = @config.client_class.enabled.find_by!(client_id: @client_id)
27
+ raise Error, "invalid_scope" if @scopes.empty?
28
+ raise Error, "invalid_scope" unless (@requested_scopes - @scopes).empty?
29
+ raise Error, "invalid_scope" unless client.allows_scopes?(@scopes)
30
+
31
+ device_code = SecureRandom.urlsafe_base64(32)
32
+ user_code = UserCode.generate(
33
+ config: @config,
34
+ exists: ->(code) { @config.grant_class.exists?(user_code: code) }
35
+ )
36
+
37
+ @config.grant_class.create!(
38
+ client_association_name => client,
39
+ :device_code_digest => @config.digest(device_code),
40
+ :user_code => user_code,
41
+ :requested_scopes => @scopes,
42
+ :status => "pending",
43
+ :challenge_expires_at => Time.current + @config.challenge_expires_in.to_i
44
+ )
45
+
46
+ verification_uri = build_verification_uri
47
+ Result.new(
48
+ device_code: device_code,
49
+ user_code: user_code,
50
+ verification_uri: verification_uri,
51
+ verification_uri_complete: "#{verification_uri}/#{user_code}",
52
+ expires_in: @config.challenge_expires_in.to_i,
53
+ interval: @config.poll_interval.to_i
54
+ )
55
+ end
56
+
57
+ private
58
+
59
+ def client_association_name
60
+ @config.client_class_name.underscore.to_sym
61
+ end
62
+
63
+ def build_verification_uri
64
+ base = (@config.verification_base_url.presence || @verification_base_url).to_s.chomp("/")
65
+ path = [@config.path_namespace, @config.verification_path].compact.join("/")
66
+ File.join(base, path)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module DeviceAuthorization
5
+ class Engine < ::Rails::Engine
6
+ initializer "active_admin.device_authorization.locales" do |app|
7
+ app.config.i18n.load_path += Dir[
8
+ root.join("config/locales/*.{rb,yml}")
9
+ ]
10
+ end
11
+
12
+ initializer "active_admin.device_authorization.filter_parameters" do |app|
13
+ app.config.filter_parameters |= [:device_code, :user_code, :access_token]
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module DeviceAuthorization
5
+ # Registers the ActiveAdmin consent page. Call after ActiveAdmin.load! setup,
6
+ # typically from config/initializers/active_admin_device_authorization.rb once
7
+ # host scopes and model class names are configured.
8
+ module Page
9
+ module_function
10
+
11
+ def register!
12
+ config = ActiveAdmin::DeviceAuthorization.config
13
+ return unless config.register_active_admin_page
14
+
15
+ menu_label = config.menu_label || -> { I18n.t("active_admin.device_authorization.menu") }
16
+ verification = config.verification_path
17
+
18
+ ActiveAdmin.register_page "Device Auth" do
19
+ menu parent: config.menu_parent,
20
+ priority: config.menu_priority,
21
+ label: menu_label
22
+
23
+ controller do
24
+ helper_method :device_auth_grant
25
+
26
+ define_method(:index) do
27
+ user_code = ActiveAdmin::DeviceAuthorization::UserCode.normalize(params[:user_code])
28
+ if user_code.present?
29
+ redirect_to send(:"#{config.path_namespace}_device_auth_grant_path", user_code)
30
+ return
31
+ end
32
+
33
+ @page_title = I18n.t("active_admin.device_authorization.enter_code")
34
+ super()
35
+ end
36
+
37
+ define_method(:show) do
38
+ load_grant!
39
+ @grant.mark_expired_if_needed!
40
+ @page_title = I18n.t("active_admin.device_authorization.title")
41
+ render "active_admin/page/index"
42
+ end
43
+
44
+ define_method(:approve) do
45
+ load_grant!
46
+ if config.before_approve && !config.before_approve.call(self)
47
+ return
48
+ end
49
+
50
+ ActiveAdmin::DeviceAuthorization::Consent.new(
51
+ grant: @grant,
52
+ resource_owner: config.current_resource_owner.call(self),
53
+ scopes: Array(params[:scopes]),
54
+ absolute_ttl_seconds: params[:absolute_ttl_seconds]
55
+ ).approve!
56
+ redirect_to send(:"#{config.path_namespace}_device_auth_grant_path", @grant.user_code),
57
+ notice: I18n.t("active_admin.device_authorization.approved")
58
+ rescue ActiveAdmin::DeviceAuthorization::Consent::Error => error
59
+ redirect_to send(:"#{config.path_namespace}_device_auth_grant_path", @grant.user_code),
60
+ alert: I18n.t(
61
+ "active_admin.device_authorization.error.#{error.message}",
62
+ default: error.message
63
+ )
64
+ end
65
+
66
+ define_method(:deny) do
67
+ load_grant!
68
+ ActiveAdmin::DeviceAuthorization::Consent.new(
69
+ grant: @grant,
70
+ resource_owner: config.current_resource_owner.call(self),
71
+ scopes: @grant.requested_scopes,
72
+ absolute_ttl_seconds: config.access_token_default_absolute_ttl
73
+ ).deny!
74
+ redirect_to send(:"#{config.path_namespace}_device_auth_grant_path", @grant.user_code),
75
+ notice: I18n.t("active_admin.device_authorization.denied")
76
+ rescue ActiveAdmin::DeviceAuthorization::Consent::Error => error
77
+ redirect_to send(:"#{config.path_namespace}_device_auth_grant_path", @grant.user_code),
78
+ alert: I18n.t(
79
+ "active_admin.device_authorization.error.#{error.message}",
80
+ default: error.message
81
+ )
82
+ end
83
+
84
+ define_method(:device_auth_grant) { @grant }
85
+
86
+ define_method(:page_presenter) do
87
+ if action_name == "show"
88
+ active_admin_config.get_page_presenter(:index)
89
+ else
90
+ super()
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ define_method(:load_grant!) do
97
+ user_code = ActiveAdmin::DeviceAuthorization::UserCode.normalize(params[:user_code])
98
+ @grant = config.grant_class.find_by!(user_code: user_code)
99
+ end
100
+ end
101
+
102
+ page_action :step_up, method: [:get, :post] do
103
+ handler = ActiveAdmin::DeviceAuthorization.config.on_approve_step_up
104
+ if handler
105
+ handler.call(self)
106
+ else
107
+ redirect_to send(:"#{ActiveAdmin::DeviceAuthorization.config.path_namespace}_device_auth_path")
108
+ end
109
+ end
110
+
111
+ content title: proc { I18n.t("active_admin.device_authorization.title") } do
112
+ if device_auth_grant
113
+ render partial: "active_admin/device_authorization/consent",
114
+ locals: {grant: device_auth_grant}
115
+ else
116
+ render partial: "active_admin/device_authorization/enter_code"
117
+ end
118
+ end
119
+ end
120
+
121
+ # Host must also route show/approve/deny; Page only registers the AA index shell.
122
+ verification
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module DeviceAuthorization
5
+ module Retention
6
+ module_function
7
+
8
+ # Deletes tokens that have been expired or revoked longer than token_retention_period.
9
+ # Absolute, idle, and revoke axes are all covered so a far-future absolute sentinel
10
+ # (absolute expiry disabled) cannot retain idle-expired rows for decades.
11
+ def call(config: ActiveAdmin::DeviceAuthorization.config)
12
+ token_cutoff = Time.current - config.token_retention_period.to_i
13
+ predicates = ["absolute_expires_at < ?", "revoked_at < ?"]
14
+ binds = [token_cutoff, token_cutoff]
15
+
16
+ if config.idle_expiry_enabled?
17
+ idle_cutoff = Time.current -
18
+ config.access_token_idle_ttl.to_i -
19
+ config.token_retention_period.to_i
20
+ predicates << "last_used_at < ?"
21
+ binds << idle_cutoff
22
+ end
23
+
24
+ config.access_token_class
25
+ .where(predicates.join(" OR "), *binds)
26
+ .in_batches(of: config.retention_batch_size)
27
+ .delete_all
28
+
29
+ grant_cutoff = Time.current - config.grant_retention_period.to_i
30
+ token_assoc = config.access_token_class_name.underscore.to_sym
31
+ config.grant_class
32
+ .where(challenge_expires_at: ...grant_cutoff)
33
+ .where.missing(token_assoc)
34
+ .in_batches(of: config.retention_batch_size)
35
+ .delete_all
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module DeviceAuthorization
5
+ module Routes
6
+ # Embed under the host admin namespace, for example:
7
+ #
8
+ # namespace :admin do
9
+ # ActiveAdmin::DeviceAuthorization::Routes.draw(self)
10
+ # end
11
+ #
12
+ # OAuth JSON endpoints always use absolute gem controller paths so nesting
13
+ # under `namespace :admin` does not invent `Admin::ActiveAdmin::...`.
14
+ #
15
+ # consent: :active_admin_page (default) routes show/approve/deny to the
16
+ # ActiveAdmin "Device Auth" page controller (`device_auth#...`).
17
+ # consent: :controller routes them to the gem ConsentsController instead.
18
+ def self.draw(mapper, config: ActiveAdmin::DeviceAuthorization.config, consent: :active_admin_page)
19
+ config.validate_rate_limits!
20
+ oauth = config.oauth_path_prefix.to_s
21
+ verification = config.verification_path.to_s
22
+
23
+ mapper.namespace :oauth, path: oauth do
24
+ mapper.post :device_authorization,
25
+ to: "/active_admin/device_authorization/device_authorizations#create"
26
+ mapper.post :token,
27
+ to: "/active_admin/device_authorization/tokens#create"
28
+ end
29
+
30
+ case consent
31
+ when :active_admin_page
32
+ mapper.get "#{verification}/:user_code",
33
+ to: "device_auth#show",
34
+ as: :device_auth_grant
35
+ mapper.post "#{verification}/:user_code/approve",
36
+ to: "device_auth#approve",
37
+ as: :approve_device_auth
38
+ mapper.post "#{verification}/:user_code/deny",
39
+ to: "device_auth#deny",
40
+ as: :deny_device_auth
41
+ when :controller
42
+ mapper.get verification,
43
+ to: "/active_admin/device_authorization/consents#index",
44
+ as: :device_auth
45
+ mapper.get "#{verification}/:user_code",
46
+ to: "/active_admin/device_authorization/consents#show",
47
+ as: :device_auth_grant
48
+ mapper.post "#{verification}/:user_code/approve",
49
+ to: "/active_admin/device_authorization/consents#approve",
50
+ as: :approve_device_auth
51
+ mapper.post "#{verification}/:user_code/deny",
52
+ to: "/active_admin/device_authorization/consents#deny",
53
+ as: :deny_device_auth
54
+ else
55
+ raise ArgumentError, "consent must be :active_admin_page or :controller (got #{consent.inspect})"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end