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