doorkeeper-device_authorization_grant 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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