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,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module DeviceAuthorizationGrant
5
+ # Device authorization endpoint for OAuth 2.0 Device Authorization Grant.
6
+ #
7
+ # @see https://tools.ietf.org/html/rfc8628#section-3.1 RFC 8628, section 3.1
8
+ class DeviceCodesController < ApplicationMetalController
9
+ def create
10
+ headers.merge!(authorize_response.headers)
11
+ render(json: authorize_response.body, status: authorize_response.status)
12
+ rescue Errors::DoorkeeperError => e
13
+ handle_token_exception(e)
14
+ end
15
+
16
+ private
17
+
18
+ def authorize_response
19
+ @authorize_response ||= strategy.authorize
20
+ end
21
+
22
+ # @return [Request::DeviceAuthorization]
23
+ def strategy
24
+ @strategy ||= Request::DeviceAuthorization.new(server)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ <div class="border-bottom mb-4">
2
+ <h1><%= t('.title') %></h1>
3
+ </div>
4
+
5
+ <%= form_with(url: oauth_device_authorizations_authorize_url) do %>
6
+ <div class="form-group row">
7
+ <%= label_tag(:user_code, t('.user_code'), class: 'col-sm-2 col-form-label font-weight-bold') %>
8
+ <div class="col-sm-10">
9
+ <%= text_field_tag(:user_code, params[:user_code], class: 'form-control') %>
10
+ </div>
11
+ </div>
12
+
13
+ <div class="form-group">
14
+ <div class="col-sm-offset-2 col-sm-10">
15
+ <%= submit_tag(t('.authorize'), class: 'btn btn-primary') %>
16
+ <%= link_to t('.cancel'), oauth_device_authorizations_index_path, class: 'btn btn-secondary' %>
17
+ </div>
18
+ </div>
19
+ <% end %>
@@ -0,0 +1,15 @@
1
+ en:
2
+ doorkeeper:
3
+ device_authorization_grant:
4
+ device_authorizations:
5
+ index:
6
+ title: 'Authorize device'
7
+ user_code: 'User code'
8
+ authorize: 'Authorize'
9
+ cancel: 'Cancel'
10
+ flash:
11
+ device_codes:
12
+ authorize:
13
+ invalid_user_code: 'The user code is invalid'
14
+ expired_user_code: 'The user code is expired'
15
+ success: 'Device successfully authorized'
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateDoorkeeperDeviceGrants < ActiveRecord::Migration[6.0]
4
+ def change
5
+ create_table :oauth_device_grants do |t|
6
+ t.references :resource_owner, null: true
7
+ t.references :application, null: false
8
+ t.string :device_code, null: false
9
+ t.string :user_code, null: true
10
+ t.integer :expires_in, null: false
11
+ t.datetime :created_at, null: false
12
+ t.datetime :last_polling_at, null: true
13
+ t.string :scopes, null: false, default: ''
14
+ end
15
+
16
+ add_index :oauth_device_grants, :device_code, unique: true
17
+ add_index :oauth_device_grants, :user_code, unique: true
18
+
19
+ add_foreign_key(
20
+ :oauth_device_grants,
21
+ :oauth_applications,
22
+ column: :application_id
23
+ )
24
+
25
+ # Uncomment below to ensure a valid reference to the resource owner's table
26
+ # add_foreign_key :oauth_device_grants, <model>, column: :resource_owner_id
27
+ end
28
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'doorkeeper'
4
+ require 'active_model'
5
+ require 'doorkeeper/device_authorization_grant/config'
6
+ require 'doorkeeper/device_authorization_grant/engine'
7
+
8
+ module Doorkeeper
9
+ # OAuth 2.0 Device Authorization Grant extension for Doorkeeper.
10
+ module DeviceAuthorizationGrant
11
+ autoload :DeviceGrant, 'doorkeeper/device_authorization_grant/orm/active_record/device_grant'
12
+ autoload :Errors, 'doorkeeper/device_authorization_grant/errors'
13
+ autoload :VERSION, 'doorkeeper/device_authorization_grant/version'
14
+
15
+ # Namespace for device authorization request strategies
16
+ module Request
17
+ autoload :DeviceAuthorization, 'doorkeeper/device_authorization_grant/request/device_authorization'
18
+ end
19
+
20
+ # Namespace for device authorization requests and responses
21
+ module OAuth
22
+ autoload :DeviceAuthorizationRequest, 'doorkeeper/device_authorization_grant/oauth/device_authorization_request'
23
+ autoload :DeviceAuthorizationResponse, 'doorkeeper/device_authorization_grant/oauth/device_authorization_response'
24
+ autoload :DeviceCodeRequest, 'doorkeeper/device_authorization_grant/oauth/device_code_request'
25
+
26
+ # Helper modules for device authorization
27
+ module Helpers
28
+ autoload :UserCode, 'doorkeeper/device_authorization_grant/oauth/helpers/user_code'
29
+ end
30
+ end
31
+
32
+ # Namespace for ORM integrations
33
+ module Orm
34
+ autoload :ActiveRecord, 'doorkeeper/device_authorization_grant/orm/active_record'
35
+ end
36
+
37
+ # Namespace for Rails integrations
38
+ module Rails
39
+ autoload :Routes, 'doorkeeper/device_authorization_grant/rails/routes'
40
+ end
41
+ end
42
+
43
+ # Doorkeeper Request namespace
44
+ module Request
45
+ autoload :DeviceCode, 'doorkeeper/request/device_code'
46
+ end
47
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module DeviceAuthorizationGrant # rubocop:disable Style/Documentation
5
+ def self.configure(&block)
6
+ if ::Doorkeeper.configuration.orm != :active_record
7
+ raise UnsupportedConfiguration, 'Doorkeeper::DeviceAuthorizationGrant only supports ActiveRecord ORM'
8
+ end
9
+
10
+ @config = Config::Builder.new(Config.new, &block).build
11
+ end
12
+
13
+ # @return [::Doorkeeper::DeviceAuthorizationGrant::Config]
14
+ def self.configuration
15
+ @config || (raise MissingConfiguration)
16
+ end
17
+
18
+ # Error raised in case of missing configuration
19
+ class MissingConfiguration < StandardError
20
+ def initialize
21
+ super('Configuration for Doorkeeper::DeviceAuthorizationGrant missing. ' \
22
+ 'Do you have Doorkeeper::DeviceAuthorizationGrant initializer?')
23
+ end
24
+ end
25
+
26
+ # Error raised when an unsupported configuration parameter is encountered
27
+ class UnsupportedConfiguration < StandardError; end
28
+
29
+ # Configuration model for Doorkeeper DeviceAuthorizationGrant
30
+ class Config
31
+ class Builder < Doorkeeper::Config::AbstractBuilder
32
+ end
33
+
34
+ def self.builder_class
35
+ Config::Builder
36
+ end
37
+
38
+ extend Doorkeeper::Config::Option
39
+
40
+ # @!attribute [r] device_code_polling_interval
41
+ # Minimum device code polling interval expected from the client, expressed in seconds.
42
+ # @return [Integer]
43
+ option :device_code_polling_interval, default: 5
44
+
45
+ # @!attribute [r] device_code_expires_in
46
+ # Device code expiration time, in seconds.
47
+ # @return [Integer]
48
+ option :device_code_expires_in, default: 300
49
+
50
+ # @!attribute [r] device_grant_class
51
+ # Customizable reference to the DeviceGrant model.
52
+ # @return [String]
53
+ option :device_grant_class, default: 'Doorkeeper::DeviceAuthorizationGrant::DeviceGrant'
54
+
55
+ # @!attribute [r] user_code_generator
56
+ # Reference to a model (or class) for user code generation.
57
+ #
58
+ # It must implement a `.generate` method, which can be invoked without
59
+ # arguments, to obtain a String user code value.
60
+ # @return [String]
61
+ option :user_code_generator, default: 'Doorkeeper::DeviceAuthorizationGrant::OAuth::Helpers::UserCode'
62
+
63
+ # @!attribute [r] verification_uri
64
+ # A Proc returning the end-user verification URI on the authorization server.
65
+ # @return [Proc]
66
+ option :verification_uri, default: ->(host_name) { "#{host_name}/oauth/device" }
67
+
68
+ # @!attribute [r] verification_uri_complete
69
+ # A Proc returning the verification URI that includes the "user_code"
70
+ # (or other information with the same function as the "user_code"), which is
71
+ # designed for non-textual transmission. This is optional, so the Proc can
72
+ # also return `nil`.
73
+ # @return [Proc]
74
+ option(
75
+ :verification_uri_complete,
76
+ default: lambda do |verification_uri, _host_name, device_grant|
77
+ "#{verification_uri}?user_code=#{CGI.escape(device_grant.user_code)}"
78
+ end
79
+ )
80
+
81
+ # @return [Class]
82
+ def device_grant_model
83
+ @device_grant_model ||= device_grant_class.constantize
84
+ end
85
+
86
+ # @return [Class, Module]
87
+ def user_code_generator_class
88
+ @user_code_generator_class ||= user_code_generator.constantize
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module DeviceAuthorizationGrant
5
+ # Doorkeeper DeviceAuthorizationGrant Rails Engine
6
+ class Engine < ::Rails::Engine
7
+ initializer 'doorkeeper.device_authorization_grant.routes' do
8
+ ::Doorkeeper::DeviceAuthorizationGrant::Rails::Routes.install!
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module DeviceAuthorizationGrant
5
+ module Errors
6
+ # The authorization request is still pending as the end user hasn't
7
+ # yet completed the user-interaction steps.
8
+ #
9
+ # The client SHOULD repeat the access token request to the token endpoint
10
+ # (a process known as polling). Before each new request, the client MUST
11
+ # wait at least the number of seconds specified by the "interval"
12
+ # parameter of the device authorization response, or 5 seconds if none
13
+ # was provided, and respect any increase in the polling interval
14
+ # required by the "slow_down" error.
15
+ class AuthorizationPending < ::Doorkeeper::Errors::DoorkeeperError
16
+ def type
17
+ :authorization_pending
18
+ end
19
+ end
20
+
21
+ # A variant of "authorization_pending", the authorization request is
22
+ # still pending and polling should continue, but the interval MUST be
23
+ # increased by 5 seconds for this and all subsequent requests.
24
+ class SlowDown < ::Doorkeeper::Errors::DoorkeeperError
25
+ def type
26
+ :slow_down
27
+ end
28
+ end
29
+
30
+ # The "device_code" has expired, and the device authorization session has
31
+ # concluded.
32
+ #
33
+ # The client MAY commence a new device authorization request but SHOULD
34
+ # wait for user interaction before restarting to avoid unnecessary
35
+ # polling.
36
+ class ExpiredToken < ::Doorkeeper::Errors::DoorkeeperError
37
+ def type
38
+ :expired_token
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module DeviceAuthorizationGrant
5
+ module OAuth
6
+ # Doorkeeper request object for handling OAuth 2.0 Device Authorization Requests.
7
+ #
8
+ # @see https://tools.ietf.org/html/rfc8628#section-3.1 RFC 8628, sect. 3.1
9
+ class DeviceAuthorizationRequest < Doorkeeper::OAuth::BaseRequest
10
+ attr_accessor :server
11
+ attr_accessor :client
12
+
13
+ # @return [String]
14
+ attr_accessor :host_name
15
+
16
+ validate :client, error: :invalid_client
17
+
18
+ # @param server
19
+ # @param client
20
+ # @param host_name [String]
21
+ def initialize(server, client, host_name)
22
+ @server = server
23
+ @client = client
24
+ @host_name = host_name
25
+ end
26
+
27
+ # @return [DeviceAuthorizationResponse, Doorkeeper::OAuth::ErrorResponse]
28
+ def authorize
29
+ validate
30
+
31
+ @response =
32
+ if valid?
33
+ destroy_expired_device_grants!
34
+ create_successful_response
35
+ else
36
+ Doorkeeper::OAuth::ErrorResponse.from_request(self)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ delegate :device_grant_model, to: :configuration
43
+
44
+ def destroy_expired_device_grants!
45
+ device_grant_model.expired.destroy_all
46
+ end
47
+
48
+ # @return [DeviceAuthorizationResponse]
49
+ def create_successful_response
50
+ before_successful_response
51
+ response = DeviceAuthorizationResponse.new(device_grant, host_name)
52
+ after_successful_response
53
+ response
54
+ end
55
+
56
+ # @return [Boolean]
57
+ def validate_client
58
+ client.present?
59
+ end
60
+
61
+ # @return [Doorkeeper::DeviceAuthorizationGrant::DeviceGrant]
62
+ def device_grant
63
+ @device_grant ||= device_grant_model.create!(device_grant_attributes)
64
+ end
65
+
66
+ # @return [Hash]
67
+ def device_grant_attributes
68
+ {
69
+ application_id: client.id,
70
+ expires_in: configuration.device_code_expires_in,
71
+ scopes: scopes.to_s,
72
+ user_code: generate_user_code
73
+ }
74
+ end
75
+
76
+ # @return [String]
77
+ def generate_user_code
78
+ configuration.user_code_generator_class.generate
79
+ end
80
+
81
+ # @return [::Doorkeeper::DeviceAuthorizationGrant::Config]
82
+ def configuration
83
+ @configuration ||= DeviceAuthorizationGrant.configuration
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module DeviceAuthorizationGrant
5
+ module OAuth
6
+ # Doorkeeper response object for handling OAuth 2.0 Device Authorization Responses.
7
+ #
8
+ # @see https://tools.ietf.org/html/rfc8628#section-3.2 RFC 8628, sect. 3.2
9
+ class DeviceAuthorizationResponse < Doorkeeper::OAuth::BaseResponse
10
+ # @return [DeviceGrant]
11
+ attr_accessor :device_grant
12
+
13
+ # @return [String]
14
+ attr_accessor :host_name
15
+
16
+ # @param device_grant [DeviceGrant]
17
+ # @param host_name [String]
18
+ def initialize(device_grant, host_name)
19
+ @device_grant = device_grant
20
+ @host_name = host_name
21
+ end
22
+
23
+ # @return [Symbol]
24
+ def status
25
+ :ok
26
+ end
27
+
28
+ # @return [Hash]
29
+ def body
30
+ {
31
+ 'device_code' => device_grant.device_code,
32
+ 'user_code' => device_grant.user_code,
33
+ 'verification_uri' => verification_uri,
34
+ 'verification_uri_complete' => verification_uri_complete,
35
+ 'expires_in' => device_grant.expires_in,
36
+ 'interval' => interval
37
+ }.reject { |_, value| value.blank? }
38
+ end
39
+
40
+ # @return [Hash]
41
+ def headers
42
+ {
43
+ 'Cache-Control' => 'no-store',
44
+ 'Pragma' => 'no-cache',
45
+ 'Content-Type' => 'application/json; charset=utf-8'
46
+ }
47
+ end
48
+
49
+ private
50
+
51
+ # @return [String]
52
+ def verification_uri
53
+ configuration.verification_uri.call(host_name)
54
+ end
55
+
56
+ # @return [String, nil]
57
+ def verification_uri_complete
58
+ configuration.verification_uri_complete.call(verification_uri, host_name, device_grant)
59
+ end
60
+
61
+ # @return [Integer, nil]
62
+ def interval
63
+ configuration.device_code_polling_interval&.seconds&.to_i
64
+ end
65
+
66
+ # @return [::Doorkeeper::DeviceAuthorizationGrant::Config]
67
+ def configuration
68
+ @configuration ||= Doorkeeper::DeviceAuthorizationGrant.configuration
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end