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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +320 -0
- data/Rakefile +34 -0
- data/app/controllers/doorkeeper/device_authorization_grant/device_authorizations_controller.rb +68 -0
- data/app/controllers/doorkeeper/device_authorization_grant/device_codes_controller.rb +28 -0
- data/app/views/doorkeeper/device_authorization_grant/device_authorizations/index.html.erb +19 -0
- data/config/locales/en.yml +15 -0
- data/db/migrate/20200629094624_create_doorkeeper_device_grants.rb +28 -0
- data/lib/doorkeeper/device_authorization_grant.rb +47 -0
- data/lib/doorkeeper/device_authorization_grant/config.rb +92 -0
- data/lib/doorkeeper/device_authorization_grant/engine.rb +12 -0
- data/lib/doorkeeper/device_authorization_grant/errors.rb +43 -0
- data/lib/doorkeeper/device_authorization_grant/oauth/device_authorization_request.rb +88 -0
- data/lib/doorkeeper/device_authorization_grant/oauth/device_authorization_response.rb +73 -0
- data/lib/doorkeeper/device_authorization_grant/oauth/device_code_request.rb +105 -0
- data/lib/doorkeeper/device_authorization_grant/oauth/helpers/user_code.rb +39 -0
- data/lib/doorkeeper/device_authorization_grant/orm/active_record.rb +27 -0
- data/lib/doorkeeper/device_authorization_grant/orm/active_record/device_grant.rb +150 -0
- data/lib/doorkeeper/device_authorization_grant/rails/routes.rb +65 -0
- data/lib/doorkeeper/device_authorization_grant/rails/routes/mapper.rb +40 -0
- data/lib/doorkeeper/device_authorization_grant/rails/routes/mapping.rb +49 -0
- data/lib/doorkeeper/device_authorization_grant/request/device_authorization.rb +38 -0
- data/lib/doorkeeper/device_authorization_grant/version.rb +8 -0
- data/lib/doorkeeper/request/device_code.rb +33 -0
- data/lib/generators/doorkeeper/device_authorization_grant/install_generator.rb +16 -0
- data/lib/generators/doorkeeper/device_authorization_grant/templates/initializer.rb +33 -0
- 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
|