doorkeeper-device_authorization_grant 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +320 -0
  4. data/Rakefile +34 -0
  5. data/app/controllers/doorkeeper/device_authorization_grant/device_authorizations_controller.rb +68 -0
  6. data/app/controllers/doorkeeper/device_authorization_grant/device_codes_controller.rb +28 -0
  7. data/app/views/doorkeeper/device_authorization_grant/device_authorizations/index.html.erb +19 -0
  8. data/config/locales/en.yml +15 -0
  9. data/db/migrate/20200629094624_create_doorkeeper_device_grants.rb +28 -0
  10. data/lib/doorkeeper/device_authorization_grant.rb +47 -0
  11. data/lib/doorkeeper/device_authorization_grant/config.rb +92 -0
  12. data/lib/doorkeeper/device_authorization_grant/engine.rb +12 -0
  13. data/lib/doorkeeper/device_authorization_grant/errors.rb +43 -0
  14. data/lib/doorkeeper/device_authorization_grant/oauth/device_authorization_request.rb +88 -0
  15. data/lib/doorkeeper/device_authorization_grant/oauth/device_authorization_response.rb +73 -0
  16. data/lib/doorkeeper/device_authorization_grant/oauth/device_code_request.rb +105 -0
  17. data/lib/doorkeeper/device_authorization_grant/oauth/helpers/user_code.rb +39 -0
  18. data/lib/doorkeeper/device_authorization_grant/orm/active_record.rb +27 -0
  19. data/lib/doorkeeper/device_authorization_grant/orm/active_record/device_grant.rb +150 -0
  20. data/lib/doorkeeper/device_authorization_grant/rails/routes.rb +65 -0
  21. data/lib/doorkeeper/device_authorization_grant/rails/routes/mapper.rb +40 -0
  22. data/lib/doorkeeper/device_authorization_grant/rails/routes/mapping.rb +49 -0
  23. data/lib/doorkeeper/device_authorization_grant/request/device_authorization.rb +38 -0
  24. data/lib/doorkeeper/device_authorization_grant/version.rb +8 -0
  25. data/lib/doorkeeper/request/device_code.rb +33 -0
  26. data/lib/generators/doorkeeper/device_authorization_grant/install_generator.rb +16 -0
  27. data/lib/generators/doorkeeper/device_authorization_grant/templates/initializer.rb +33 -0
  28. metadata +139 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 41a11fbc4e3f17a41ba597414e90d134d92f121ed56ee0ab3fbd3289d5a8c000
4
+ data.tar.gz: 7662d33b032f9293c09ebdcfeef4afc2e6fabdbb5da6adf36baf2ac48d2d1ab6
5
+ SHA512:
6
+ metadata.gz: c7c692bfa261c432498e68a300f657ed532471f6c3414b8d7def3c1c27c1f0e3424a3ddba99c19d6eb274d6aecd8a5f6d14d5a593e651bdb659aa455498a8ccb
7
+ data.tar.gz: b7de6fc069d86e5c2ec3dd3e2f94dc645ffef57934747dd2d9d3ee30bc3a0c7b529632c57e215de3302988db49ef88e994f08eafdc161327c87268bbec205ddf
@@ -0,0 +1,20 @@
1
+ Copyright 2020 EXOP Group
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,320 @@
1
+ # Doorkeeper::DeviceAuthorizationGrant
2
+
3
+ OAuth 2.0 device authorization grant extension for Doorkeeper.
4
+
5
+ This library implements the OAuth 2.0 device authorization grant
6
+ ([RFC 8628](https://tools.ietf.org/html/rfc8628)) for
7
+ [Ruby on Rails](https://rubyonrails.org/) applications on top of the
8
+ [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper) OAuth 2.0 framework.
9
+
10
+ ## Status
11
+
12
+ This extension currently works with Doorkeeper version `>= 5.4.0`.
13
+
14
+ As of June 25 2020, due to some limitations of Doorkeeper, it is currently
15
+ inconvenient for this Gem to use the official OAuth grant type
16
+ `urn:ietf:params:oauth:grant-type:device_code`. Instead, it has been renamed
17
+ to simply `device_code`, which is non-standard. This is going to be corrected
18
+ soon: the next Doorkeeper release will include the ability to cleanly
19
+ register custom Grant Flows - see [Doorkeeper Pull Request #1418](https://github.com/doorkeeper-gem/doorkeeper/pull/1418).
20
+
21
+ ## Installation
22
+
23
+ Add this line to your application's Gemfile:
24
+
25
+ ```ruby
26
+ gem 'doorkeeper-device_authorization_grant'
27
+ ```
28
+
29
+ And then execute:
30
+
31
+ ```bash
32
+ $ bundle
33
+ ```
34
+
35
+ Or install it yourself as:
36
+
37
+ ```bash
38
+ $ gem install doorkeeper-device_authorization_grant
39
+ ```
40
+
41
+ Run the installation generator to update routes and create a dedicated initializer:
42
+
43
+ ```bash
44
+ $ rails generate doorkeeper:device_authorization_grant:install
45
+ ```
46
+
47
+ Generate a migration for Active Record (other ORMs are currently not supported):
48
+
49
+ ```bash
50
+ $ rails doorkeeper_device_authorization_grant_engine:install:migrations
51
+ ```
52
+
53
+ ## Configuration
54
+
55
+ ### Doorkeeper configuration
56
+
57
+ In your Doorkeeper initializer (usually `config/initializers/doorkeeper.rb`), enable
58
+ the device flow extension grant type, adding to the `grant_flows` option the `device_code`
59
+ string. For example:
60
+
61
+ ```ruby
62
+ # config/initializers/doorkeeper.rb
63
+
64
+ Doorkeeper.configure do
65
+ # ...
66
+
67
+ grant_flows [
68
+ # Note: this is a non-standard grant flow, used instead of the
69
+ # official `urn:ietf:params:oauth:grant-type:device_code` due to
70
+ # current Doorkeeper limitations.
71
+ 'device_code',
72
+
73
+ # together with all the other grant flows you already enabled, for example:
74
+ 'authorization_code',
75
+ 'client_credentials'
76
+ # ...
77
+ ]
78
+
79
+ # ...
80
+ end
81
+ ```
82
+
83
+ Please note that **this is not the official grant flow**. The real one should be
84
+ the IANA URN `urn:ietf:params:oauth:grant-type:device_code`, however this is hard
85
+ to support with the current version of Doorkeeper, due to how strategy classes are
86
+ looked up by grant type value.
87
+
88
+ ### Device Authorization Grant configuration
89
+
90
+ The gem's installation scripts automatically creates a new initializer file:
91
+ `config/initializers/doorkeeper_device_authorization_grant.rb`. Here you can
92
+ adjust the configuration parameters according to your needs.
93
+
94
+ ### Routes
95
+
96
+ The gem's installation scripts automatically modify your `config/routes.rb`
97
+ file, adding the default routes to the controllers described above. The
98
+ routes file should then look like this:
99
+
100
+ ```ruby
101
+ Rails.application.routes.draw do
102
+ use_doorkeeper_device_authorization_grant
103
+ # your routes ...
104
+ end
105
+ ```
106
+
107
+ This is enough to add to your app the following default routes:
108
+
109
+ ```
110
+ Prefix Verb URI Controller#Action
111
+ oauth_device_codes_create POST /oauth/authorize_device doorkeeper/device_authorization_grant/device_codes#create
112
+ oauth_device_authorizations_index GET /oauth/device doorkeeper/device_authorization_grant/device_authorizations#index
113
+ oauth_device_authorizations_authorize POST /oauth/device doorkeeper/device_authorization_grant/device_authorizations#authorize
114
+ ```
115
+
116
+ The routing method `use_doorkeeper_device_authorization_grant` allows extra customization,
117
+ just like `use_doorkeeper` (see [Doorkeeper Wiki - Customizing routes](https://github.com/doorkeeper-gem/doorkeeper/wiki/Customizing-routes)).
118
+
119
+ This Gem defines two Rails controllers:
120
+
121
+ - `DeviceCodesController` serves Device Authorization requests, as described
122
+ by [RFC 8628](https://tools.ietf.org/html/rfc8628), sections 3.1
123
+ and 3.2.
124
+ - `DeviceAuthorizationsController` provides a bare-bones implementation of a
125
+ verification web page which allows an authenticated resource-owner to
126
+ authorize a device, by providing an end-user code.
127
+
128
+ You can change the controllers to your **custom controllers** with the `controller` option:
129
+
130
+ ```ruby
131
+ Rails.application.routes.draw do
132
+ use_doorkeeper_device_authorization_grant do
133
+ # it accepts :device_authorizations and :device_codes
134
+ controller device_authorizations: 'custom_device_authorizations'
135
+ end
136
+ end
137
+ ```
138
+
139
+ Be sure to use the same superclasses of the original controllers (or something compatible).
140
+
141
+ You can set **custom aliases** with `as`:
142
+
143
+ ```ruby
144
+ Rails.application.routes.draw do
145
+ use_doorkeeper_device_authorization_grant do
146
+ # it accepts :device_authorizations and :device_codes
147
+ as device_codes: :custom_device
148
+ end
149
+ end
150
+ ```
151
+
152
+ You can **skip routes** with `skip_controllers`:
153
+
154
+ ```ruby
155
+ Rails.application.routes.draw do
156
+ use_doorkeeper_device_authorization_grant do
157
+ # it accepts :device_authorizations and :device_codes
158
+ skip_controllers :device_authorizations
159
+ end
160
+ end
161
+ ```
162
+
163
+ The default scope is `oauth`. You can provide a **custom scope** like this:
164
+
165
+ ```ruby
166
+ Rails.application.routes.draw do
167
+ use_doorkeeper_device_authorization_grant scope: 'oauth2'
168
+ end
169
+ ```
170
+
171
+ ## Usage
172
+
173
+ The following sections show the typical steps of a device authorization flow.
174
+ Default configuration and routes are assumed.
175
+
176
+ ### Device Authorization Request
177
+
178
+ Reference: [RFC 8628, section 3.1 - Device Authorization Request](https://tools.ietf.org/html/rfc8628#section-3.1).
179
+
180
+ First of all, a *Device Client* can perform a *Device Authorization Request* to
181
+ the *Authorization Server* (your Rails application, with Doorkeeper and this
182
+ gem extension) like this:
183
+
184
+ ```http request
185
+ POST /oauth/authorize_device HTTP/1.1
186
+ Content-Type: application/x-www-form-urlencoded
187
+
188
+ client_id=1406020730&scope=example_scope
189
+ ```
190
+
191
+ ### Device Authorization Response
192
+
193
+ Reference: [RFC 8628, section 3.2 - Device Authorization Response](https://tools.ietf.org/html/rfc8628#section-3.2).
194
+
195
+ The *Authorization Server* responds with a *Device Authorization Response*:
196
+
197
+ ```
198
+ HTTP/1.1 200 OK
199
+ Content-Type: application/json
200
+
201
+ {
202
+ "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
203
+ "user_code": "0A44L90H",
204
+ "verification_uri": "https://example.com/oauth/device",
205
+ "verification_uri_complete": "https://example.com/oauth/device?user_code=0A44L90H",
206
+ "expires_in": 300,
207
+ "interval": 5
208
+ }
209
+ ```
210
+
211
+ ### User interaction
212
+
213
+ Reference: [RFC 8628, section 3.3 - User Interaction](https://tools.ietf.org/html/rfc8628#section-3.3).
214
+
215
+ The *Device Client* can now display to the end user the `user_code` and the
216
+ `verification_uri` (or somehow make use of `verification_uri_complete`, in special cases).
217
+
218
+ The user should visit URI in a user agent on a secondary device (for example, in a browser
219
+ on their mobile phone) and enter the user code.
220
+
221
+ During the user interaction, the device continuously polls the token endpoint with the
222
+ `device_code`, as detailed in the next section, until the user completes the interaction,
223
+ the code expires, or another error occurs.
224
+
225
+ The default Rails route provided by this Gem, `/oauth/device`, allows an authenticated
226
+ request owner (for example, a user) to manually verify the user code.
227
+
228
+ ### Device Access Token Request / polling
229
+
230
+ Reference: [RFC 8628, section 3.4 - Device Access Token Request](https://tools.ietf.org/html/rfc8628#section-3.4).
231
+
232
+ After displaying instructions to the user, the *Device Client* should create a
233
+ *Device Access Token Request* and send it to the token endpoint (provided
234
+ by Dorkeeper), for example:
235
+
236
+ ```http request
237
+ POST /oauth/token HTTP/1.1
238
+ Content-Type: application/x-www-form-urlencoded
239
+
240
+ grant_type=device_code
241
+ &device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS
242
+ &client_id=1406020730
243
+ ```
244
+
245
+ **Note:** this is a non-standard `grant_type`, used instead of the official
246
+ `urn:ietf:params:oauth:grant-type:device_code` due to current limitations of
247
+ the Doorkeeper gem.
248
+
249
+ The response to this request is defined in the next section. It is expected for
250
+ the *Device Client* to try the access token request repeatedly in a polling
251
+ fashion, based on the error code in the response. The polling time interval
252
+ was possibly included in the *Device Authorization Response*, but it is
253
+ optional; if no value was provided, the client MUST use 5 seconds as the default.
254
+
255
+ ### Device Access Token Response
256
+
257
+ Reference: [RFC 8628, section 3.5 - Device Access Token Response](https://tools.ietf.org/html/rfc8628#section-3.5).
258
+
259
+ Please refer to the RFC document for exhaustive documentation. Here we show just
260
+ some possible responses.
261
+
262
+ While the authorization request is still pending, and the device-code token is
263
+ not expired, the response contains an `authorization_pending` error:
264
+
265
+ ```
266
+ HTTP/1.1 400 Bad Request
267
+ Content-Type: application/json
268
+
269
+ { "error": "authorization_pending", "error_description": "..." }
270
+ ```
271
+
272
+ The client should simply continue with further polling requests.
273
+
274
+ If the client requests are too close in time, a `slow_down` error is returned:
275
+
276
+ ```
277
+ HTTP/1.1 400 Bad Request
278
+ Content-Type: application/json
279
+
280
+ { "error": "slow_down", "error_description": "..." }
281
+ ```
282
+
283
+ The client can still continue with polling requests, but the polling time interval
284
+ MUST be increased by 5 seconds for all subsequent requests.
285
+
286
+ If the `device_code` has expired, the response contains the `expired_token` error:
287
+
288
+ ```
289
+ HTTP/1.1 400 Bad Request
290
+ Content-Type: application/json
291
+
292
+ { "error": "expired_token", "error_description": "..." }
293
+ ```
294
+
295
+ The client should stop polling, and may commence a new device authorization
296
+ request (possibly upon waiting for further user interaction).
297
+
298
+ Once the user has successfully authorized the device, a successful response will
299
+ be eventually returned. This is a standard OAuth 2.0 response, described in
300
+ [Section 5.1 of [RFC6749]](https://tools.ietf.org/html/rfc6749#section-5.1). Here
301
+ is a typical bearer token response:
302
+
303
+ ```
304
+ HTTP/1.1 200 OK
305
+ Content-Type: application/json
306
+
307
+ {
308
+ "access_token": "FkPeBMF8Ab0zkYj6vQLZCxZ5OP0Hrd7ST3RS99x7nRM",
309
+ "token_type": "Bearer",
310
+ "expires_in": 7200,
311
+ "scope": "read",
312
+ "created_at": 1593096829
313
+ }
314
+ ```
315
+
316
+ The device authentication flow is now complete, and the token data can be used to
317
+ authenticate requests against the authorization and/or resource server.
318
+
319
+ ## License
320
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ require 'rdoc/task'
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = 'rdoc'
13
+ rdoc.title = 'Doorkeeper::DeviceAuthorizationGrant'
14
+ rdoc.options << '--line-numbers'
15
+ rdoc.rdoc_files.include('README.md')
16
+ rdoc.rdoc_files.include('lib/**/*.rb')
17
+ end
18
+
19
+ APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
20
+ load 'rails/tasks/engine.rake'
21
+
22
+ load 'rails/tasks/statistics.rake'
23
+
24
+ require 'bundler/gem_tasks'
25
+
26
+ require 'rake/testtask'
27
+
28
+ Rake::TestTask.new(:test) do |t|
29
+ t.libs << 'test'
30
+ t.pattern = 'test/**/*_test.rb'
31
+ t.verbose = false
32
+ end
33
+
34
+ task default: :test
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module DeviceAuthorizationGrant
5
+ # The Device Authorizations controller provides a simple interface which
6
+ # allows authenticated resource owners to authorize devices, by providing
7
+ # a user code.
8
+ class DeviceAuthorizationsController < Doorkeeper::ApplicationController
9
+ before_action :authenticate_resource_owner!
10
+
11
+ def index
12
+ respond_to do |format|
13
+ format.html
14
+ format.json { head :no_content }
15
+ end
16
+ end
17
+
18
+ def authorize
19
+ device_grant_model.transaction do
20
+ device_grant = device_grant_model.lock.find_by(user_code: user_code)
21
+ return authorization_error_response(:invalid_user_code) if device_grant.nil?
22
+ return authorization_error_response(:expired_user_code) if device_grant.expired?
23
+
24
+ device_grant.update!(user_code: nil, resource_owner_id: current_resource_owner.id)
25
+ end
26
+
27
+ authorization_success_response
28
+ end
29
+
30
+ private
31
+
32
+ def authorization_success_response
33
+ respond_to do |format|
34
+ notice = I18n.t(:success, scope: i18n_flash_scope(:authorize))
35
+ format.html { redirect_to oauth_device_authorizations_index_url, notice: notice }
36
+ format.json { head :no_content }
37
+ end
38
+ end
39
+
40
+ # @param error_message_key [Symbol]
41
+ def authorization_error_response(error_message_key)
42
+ respond_to do |format|
43
+ notice = I18n.t(error_message_key, scope: i18n_flash_scope(:authorize))
44
+ format.html { redirect_to oauth_device_authorizations_index_url, notice: notice }
45
+ format.json do
46
+ render json: { errors: [notice] }, status: :unprocessable_entity
47
+ end
48
+ end
49
+ end
50
+
51
+ # @return [Class]
52
+ def device_grant_model
53
+ @device_grant_model ||= Doorkeeper::DeviceAuthorizationGrant.configuration.device_grant_model
54
+ end
55
+
56
+ # @return [String, nil]
57
+ def user_code
58
+ params[:user_code]
59
+ end
60
+
61
+ # @param action [Symbol]
62
+ # @return [Array<Symbol>]
63
+ def i18n_flash_scope(action)
64
+ %I[doorkeeper flash device_codes #{action}]
65
+ end
66
+ end
67
+ end
68
+ end