doorkeeper 4.3.2 → 4.4.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of doorkeeper might be problematic. Click here for more details.

Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/NEWS.md +17 -0
  4. data/app/controllers/doorkeeper/applications_controller.rb +2 -1
  5. data/app/controllers/doorkeeper/tokens_controller.rb +9 -10
  6. data/app/views/doorkeeper/applications/_form.html.erb +11 -0
  7. data/app/views/doorkeeper/applications/index.html.erb +2 -0
  8. data/app/views/doorkeeper/applications/show.html.erb +3 -0
  9. data/config/locales/en.yml +6 -0
  10. data/doorkeeper.gemspec +2 -0
  11. data/lib/doorkeeper/config.rb +14 -0
  12. data/lib/doorkeeper/models/access_token_mixin.rb +1 -1
  13. data/lib/doorkeeper/models/application_mixin.rb +8 -1
  14. data/lib/doorkeeper/oauth/client/credentials.rb +3 -1
  15. data/lib/doorkeeper/oauth/helpers/uri_checker.rb +1 -0
  16. data/lib/doorkeeper/oauth/pre_authorization.rb +5 -3
  17. data/lib/doorkeeper/orm/active_record/application.rb +17 -0
  18. data/lib/doorkeeper/rails/routes.rb +5 -1
  19. data/lib/doorkeeper/request/password.rb +1 -11
  20. data/lib/doorkeeper/version.rb +23 -2
  21. data/lib/generators/doorkeeper/add_client_confidentiality_generator.rb +31 -0
  22. data/lib/generators/doorkeeper/templates/add_confidential_to_application_migration.rb.erb +11 -0
  23. data/lib/generators/doorkeeper/templates/migration.rb.erb +1 -0
  24. data/spec/controllers/authorizations_controller_spec.rb +34 -2
  25. data/spec/controllers/tokens_controller_spec.rb +59 -7
  26. data/spec/dummy/config/initializers/doorkeeper.rb +5 -0
  27. data/spec/dummy/db/migrate/20111122132257_create_users.rb +3 -1
  28. data/spec/dummy/db/migrate/20120312140401_add_password_to_users.rb +3 -1
  29. data/spec/dummy/db/migrate/20151223192035_create_doorkeeper_tables.rb +3 -1
  30. data/spec/dummy/db/migrate/20151223200000_add_owner_to_application.rb +3 -1
  31. data/spec/dummy/db/migrate/20160320211015_add_previous_refresh_token_to_access_tokens.rb +3 -1
  32. data/spec/dummy/db/migrate/20180210183654_add_confidential_to_application.rb +13 -0
  33. data/spec/dummy/db/schema.rb +2 -1
  34. data/spec/lib/config_spec.rb +25 -0
  35. data/spec/lib/oauth/authorization_code_request_spec.rb +15 -0
  36. data/spec/lib/oauth/client/credentials_spec.rb +4 -2
  37. data/spec/lib/oauth/helpers/uri_checker_spec.rb +5 -0
  38. data/spec/lib/oauth/pre_authorization_spec.rb +12 -7
  39. data/spec/models/doorkeeper/application_spec.rb +96 -5
  40. data/spec/requests/flows/authorization_code_spec.rb +1 -1
  41. data/spec/requests/flows/password_spec.rb +64 -21
  42. metadata +25 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c3122dbcfcc7470b2460319fee999036c26810c3
4
- data.tar.gz: 9c0096009a2af6d32074d63711fb72a2078101a1
3
+ metadata.gz: 8d8d3550d8406d4abb224c4960d1d6e8a0c4c706
4
+ data.tar.gz: b12408cb8b0dc2b14ee69b57798943b5c1bfaa30
5
5
  SHA512:
6
- metadata.gz: 7178420e74461146ac3525056a9398e51df2e659107532e1e8577e90359d474a96b1275d99f05dc85b43f692ab9eda1e49bb1edc91c80f906d59318c5e788163
7
- data.tar.gz: 5c6a9177e6efd616f7345bc26e15a978ae7d89295600356bf9ec922a0c94f0da1b47fe827328d7edcbcafe0855761a17a77851d3a1b9155d0c8608917c3e4610
6
+ metadata.gz: 0674af950f6070d6457e09f73fc89736b092ae6595e484ca6e67e7f126912ea007509d9249fdc4eb01e66bf981c1e49da33712203d8428d10401a43faabd1cfd
7
+ data.tar.gz: e447513c202dfde4c622b898da2a98dff64272193136fe399b890bb97488e7915156a2588caa6de3566db411f4c7dfa89e88be3a8b8d0a76511251f2f980c382
data/.rubocop.yml CHANGED
@@ -2,6 +2,10 @@ AllCops:
2
2
  Exclude:
3
3
  - "spec/dummy/db/*"
4
4
 
5
+ Metrics/BlockLength:
6
+ Exclude:
7
+ - spec/**/*
8
+
5
9
  LineLength:
6
10
  Exclude:
7
11
  - spec/**/*
data/NEWS.md CHANGED
@@ -4,6 +4,23 @@ User-visible changes worth mentioning.
4
4
 
5
5
  ## master
6
6
 
7
+ ## 4.4.3
8
+ - [#1143] Adds a config option opt_out_native_route_change to opt out of the
9
+ breaking api changed introduced in
10
+ https://github.com/doorkeeper-gem/doorkeeper/pull/1003
11
+
12
+ ## 4.4.2
13
+ - [#1130] Backport fix for native redirect_uri from 5.x.
14
+
15
+ ## 4.4.1
16
+
17
+ - [#1127] Backport token type to comply with the RFC6750 specification.
18
+ - [#1125] Backport Quote surround I18n yes/no keys
19
+
20
+ ## 4.4.0
21
+
22
+ - [#1120] Backport security fix from 5.x for token revocation when using public clients
23
+
7
24
  ## 4.3.2
8
25
 
9
26
  - [#1053] Support authorizing with query params in the request `redirect_uri` if explicitly present in app's `Application#redirect_uri`
@@ -57,7 +57,8 @@ module Doorkeeper
57
57
  end
58
58
 
59
59
  def application_params
60
- params.require(:doorkeeper_application).permit(:name, :redirect_uri, :scopes)
60
+ params.require(:doorkeeper_application).
61
+ permit(:name, :redirect_uri, :scopes, :confidential)
61
62
  end
62
63
  end
63
64
  end
@@ -58,16 +58,15 @@ module Doorkeeper
58
58
  # https://tools.ietf.org/html/rfc6749#section-2.1
59
59
  # https://tools.ietf.org/html/rfc7009
60
60
  def authorized?
61
- if token.present?
62
- # Client is confidential, therefore client authentication & authorization
63
- # is required
64
- if token.application_id?
65
- # We authorize client by checking token's application
66
- server.client && server.client.application == token.application
67
- else
68
- # Client is public, authentication unnecessary
69
- true
70
- end
61
+ return unless token.present?
62
+ # Client is confidential, therefore client authentication & authorization
63
+ # is required
64
+ if token.application_id? && token.application.confidential?
65
+ # We authorize client by checking token's application
66
+ server.client && server.client.application == token.application
67
+ else
68
+ # Client is public, authentication unnecessary
69
+ true
71
70
  end
72
71
  end
73
72
 
@@ -27,6 +27,17 @@
27
27
  </div>
28
28
  <% end %>
29
29
 
30
+ <%= content_tag :div, class: "form-group#{' has-error' if application.errors[:confidential].present?}" do %>
31
+ <%= f.label :confidential, class: 'col-sm-2 control-label' %>
32
+ <div class="col-sm-10">
33
+ <%= f.check_box :confidential, class: 'form-control', disabled: !Doorkeeper::Application.supports_confidentiality? %>
34
+ <%= doorkeeper_errors_for application, :confidential %>
35
+ <span class="help-block">
36
+ <%= t('doorkeeper.applications.help.confidential') %>
37
+ </span>
38
+ </div>
39
+ <% end %>
40
+
30
41
  <%= content_tag :div, class: "form-group#{' has-error' if application.errors[:scopes].present?}" do %>
31
42
  <%= f.label :scopes, class: 'col-sm-2 control-label' %>
32
43
  <div class="col-sm-10">
@@ -9,6 +9,7 @@
9
9
  <tr>
10
10
  <th><%= t('.name') %></th>
11
11
  <th><%= t('.callback_url') %></th>
12
+ <th><%= t('.confidential') %></th>
12
13
  <th></th>
13
14
  <th></th>
14
15
  </tr>
@@ -18,6 +19,7 @@
18
19
  <tr id="application_<%= application.id %>">
19
20
  <td><%= link_to application.name, oauth_application_path(application) %></td>
20
21
  <td><%= application.redirect_uri %></td>
22
+ <td><%= application.confidential? ? t('doorkeeper.applications.index.confidentiality.yes') : t('doorkeeper.applications.index.confidentiality.no') %></td>
21
23
  <td><%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(application), class: 'btn btn-link' %></td>
22
24
  <td><%= render 'delete_form', application: application %></td>
23
25
  </tr>
@@ -13,6 +13,9 @@
13
13
  <h4><%= t('.scopes') %>:</h4>
14
14
  <p><code id="scopes"><%= @application.scopes %></code></p>
15
15
 
16
+ <h4><%= t('.confidential') %>:</h4>
17
+ <p><code id="confidential"><%= @application.confidential? %></code></p>
18
+
16
19
  <h4><%= t('.callback_urls') %>:</h4>
17
20
 
18
21
  <table>
@@ -28,6 +28,7 @@ en:
28
28
  form:
29
29
  error: 'Whoops! Check your form for possible errors'
30
30
  help:
31
+ confidential: 'Application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential.'
31
32
  redirect_uri: 'Use one line per URI'
32
33
  native_redirect_uri: 'Use %{native_redirect_uri} if you want to add localhost URIs for development purposes'
33
34
  scopes: 'Separate scopes with spaces. Leave blank to use the default scopes.'
@@ -38,6 +39,10 @@ en:
38
39
  new: 'New Application'
39
40
  name: 'Name'
40
41
  callback_url: 'Callback URL'
42
+ confidential: 'Confidential?'
43
+ confidentiality:
44
+ 'yes': 'Yes'
45
+ 'no': 'No'
41
46
  new:
42
47
  title: 'New Application'
43
48
  show:
@@ -45,6 +50,7 @@ en:
45
50
  application_id: 'Application Id'
46
51
  secret: 'Secret'
47
52
  scopes: 'Scopes'
53
+ confidential: 'Confidential'
48
54
  callback_urls: 'Callback urls'
49
55
  actions: 'Actions'
50
56
 
data/doorkeeper.gemspec CHANGED
@@ -27,4 +27,6 @@ Gem::Specification.new do |s|
27
27
  s.add_development_dependency "generator_spec", "~> 0.9.3"
28
28
  s.add_development_dependency "rake", ">= 11.3.0"
29
29
  s.add_development_dependency "rspec-rails"
30
+
31
+ s.post_install_message = Doorkeeper::CVE_2018_1000211_WARNING
30
32
  end
@@ -114,6 +114,15 @@ doorkeeper.
114
114
  def reuse_access_token
115
115
  @config.instance_variable_set(:@reuse_access_token, true)
116
116
  end
117
+
118
+ # Opt out of breaking api change to the native authorization code flow.
119
+ # Opting out sets the authorization code response route for native
120
+ # redirect uris to oauth/authorize/<code>. The default is
121
+ # oauth/authorize/native?code=<code>.
122
+ # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1143
123
+ def opt_out_native_route_change
124
+ @config.instance_variable_set(:@opt_out_native_route_change, true)
125
+ end
117
126
  end
118
127
 
119
128
  module Option
@@ -295,6 +304,11 @@ doorkeeper.
295
304
  @token_grant_types ||= calculate_token_grant_types
296
305
  end
297
306
 
307
+ def native_authorization_code_route
308
+ @opt_out_native_route_change ||= false
309
+ @opt_out_native_route_change ? '/:code' : '/native'
310
+ end
311
+
298
312
  private
299
313
 
300
314
  # Determines what values are acceptable for 'response_type' param in
@@ -156,7 +156,7 @@ module Doorkeeper
156
156
  # The OAuth 2.0 Authorization Framework: Bearer Token Usage
157
157
  #
158
158
  def token_type
159
- 'bearer'
159
+ 'Bearer'
160
160
  end
161
161
 
162
162
  def use_refresh_token?
@@ -10,6 +10,9 @@ module Doorkeeper
10
10
  # Returns an instance of the Doorkeeper::Application with
11
11
  # specific UID and secret.
12
12
  #
13
+ # Public/Non-confidential applications will only find by uid if secret is
14
+ # blank.
15
+ #
13
16
  # @param uid [#to_s] UID (any object that responds to `#to_s`)
14
17
  # @param secret [#to_s] secret (any object that responds to `#to_s`)
15
18
  #
@@ -17,7 +20,11 @@ module Doorkeeper
17
20
  # if there is no record with such credentials
18
21
  #
19
22
  def by_uid_and_secret(uid, secret)
20
- find_by(uid: uid.to_s, secret: secret.to_s)
23
+ app = by_uid(uid)
24
+ return unless app
25
+ return app if secret.blank? && !app.confidential?
26
+ return unless app.secret == secret
27
+ app
21
28
  end
22
29
 
23
30
  # Returns an instance of the Doorkeeper::Application with specific UID.
@@ -23,8 +23,10 @@ module Doorkeeper
23
23
  end
24
24
  end
25
25
 
26
+ # Public clients may have their secret blank, but "credentials" are
27
+ # still present
26
28
  def blank?
27
- uid.blank? || secret.blank?
29
+ uid.blank?
28
30
  end
29
31
  end
30
32
  end
@@ -3,6 +3,7 @@ module Doorkeeper
3
3
  module Helpers
4
4
  module URIChecker
5
5
  def self.valid?(url)
6
+ return true if native_uri?(url)
6
7
  uri = as_uri(url)
7
8
  uri.fragment.nil? && !uri.host.nil? && !uri.scheme.nil?
8
9
  rescue URI::InvalidURIError
@@ -57,9 +57,11 @@ module Doorkeeper
57
57
 
58
58
  # TODO: test uri should be matched against the client's one
59
59
  def validate_redirect_uri
60
- return false unless redirect_uri.present?
61
- Helpers::URIChecker.native_uri?(redirect_uri) ||
62
- Helpers::URIChecker.valid_for_authorization?(redirect_uri, client.redirect_uri)
60
+ return false if redirect_uri.blank?
61
+
62
+ Helpers::URIChecker.valid_for_authorization?(
63
+ redirect_uri, client.redirect_uri
64
+ )
63
65
  end
64
66
  end
65
67
  end
@@ -11,6 +11,7 @@ module Doorkeeper
11
11
  validates :name, :secret, :uid, presence: true
12
12
  validates :uid, uniqueness: true
13
13
  validates :redirect_uri, redirect_uri: true
14
+ validates :confidential, inclusion: { in: [true, false] }
14
15
 
15
16
  before_validation :generate_uid, :generate_secret, on: :create
16
17
 
@@ -31,6 +32,22 @@ module Doorkeeper
31
32
  where(id: resource_access_tokens.select(:application_id).distinct)
32
33
  end
33
34
 
35
+ # Fallback to existing, default behaviour of assuming all apps to be
36
+ # confidential if the migration hasn't been run
37
+ def confidential
38
+ return super if self.class.supports_confidentiality?
39
+ ActiveSupport::Deprecation.warn 'You are susceptible to security bug ' \
40
+ 'CVE-2018-1000211. Please follow instructions outlined in ' \
41
+ 'Doorkeeper::CVE_2018_1000211_WARNING'
42
+ true
43
+ end
44
+
45
+ alias_method :confidential?, :confidential
46
+
47
+ def self.supports_confidentiality?
48
+ column_names.include?('confidential')
49
+ end
50
+
34
51
  private
35
52
 
36
53
  def generate_uid
@@ -47,7 +47,7 @@ module Doorkeeper
47
47
  as: mapping[:as],
48
48
  controller: mapping[:controllers]
49
49
  ) do
50
- routes.get '/native', action: :show, on: :member
50
+ routes.get native_authorization_code_route, action: :show, on: :member
51
51
  routes.get '/', action: :new, on: :member
52
52
  end
53
53
  end
@@ -85,6 +85,10 @@ module Doorkeeper
85
85
  def authorized_applications_routes(mapping)
86
86
  routes.resources :authorized_applications, only: %i[index destroy], controller: mapping[:controllers]
87
87
  end
88
+
89
+ def native_authorization_code_route
90
+ Doorkeeper.configuration.native_authorization_code_route
91
+ end
88
92
  end
89
93
  end
90
94
  end
@@ -3,7 +3,7 @@ require 'doorkeeper/request/strategy'
3
3
  module Doorkeeper
4
4
  module Request
5
5
  class Password < Strategy
6
- delegate :credentials, :resource_owner, :parameters, to: :server
6
+ delegate :credentials, :resource_owner, :parameters, :client, to: :server
7
7
 
8
8
  def request
9
9
  @request ||= OAuth::PasswordAccessTokenRequest.new(
@@ -13,16 +13,6 @@ module Doorkeeper
13
13
  parameters
14
14
  )
15
15
  end
16
-
17
- private
18
-
19
- def client
20
- if credentials
21
- server.client
22
- elsif parameters[:client_id]
23
- server.client_via_uid
24
- end
25
- end
26
16
  end
27
17
  end
28
18
  end
@@ -1,4 +1,25 @@
1
1
  module Doorkeeper
2
+ CVE_2018_1000211_WARNING = <<-HEREDOC.freeze
3
+
4
+
5
+ WARNING: This is a security release that addresses token revocation not working for public apps (CVE-2018-1000211)
6
+
7
+ There is no breaking change in this release, however to take advantage of the security fix you must:
8
+
9
+ 1. Run `rails generate doorkeeper:add_client_confidentiality` for the migration
10
+ 2. Review your OAuth apps and determine which ones exclusively use public grant flows (eg implicit)
11
+ 3. Update their `confidential` column to `false` for those public apps
12
+
13
+ This is a backported security release.
14
+
15
+ For more information:
16
+
17
+ * https://github.com/doorkeeper-gem/doorkeeper/pull/1119
18
+ * https://github.com/doorkeeper-gem/doorkeeper/issues/891
19
+
20
+
21
+ HEREDOC
22
+
2
23
  def self.gem_version
3
24
  Gem::Version.new VERSION::STRING
4
25
  end
@@ -6,8 +27,8 @@ module Doorkeeper
6
27
  module VERSION
7
28
  # Semantic versioning
8
29
  MAJOR = 4
9
- MINOR = 3
10
- TINY = 2
30
+ MINOR = 4
31
+ TINY = 3
11
32
 
12
33
  # Full version number
13
34
  STRING = [MAJOR, MINOR, TINY].compact.join('.')
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/active_record'
4
+
5
+ module Doorkeeper
6
+ class AddClientConfidentialityGenerator < ::Rails::Generators::Base
7
+ include ::Rails::Generators::Migration
8
+ source_root File.expand_path('templates', __dir__)
9
+ desc 'Adds a migration to fix CVE-2018-1000211.'
10
+
11
+ def install
12
+ migration_template(
13
+ 'add_confidential_to_application_migration.rb.erb',
14
+ 'db/migrate/add_confidential_to_doorkeeper_application.rb',
15
+ migration_version: migration_version
16
+ )
17
+ end
18
+
19
+ def self.next_migration_number(dirname)
20
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
21
+ end
22
+
23
+ private
24
+
25
+ def migration_version
26
+ if ::ActiveRecord::VERSION::MAJOR >= 5
27
+ "[#{::ActiveRecord::VERSION::MAJOR}.#{::ActiveRecord::VERSION::MINOR}]"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,11 @@
1
+ class AddConfidentialToDoorkeeperApplication < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ add_column(
4
+ :oauth_applications,
5
+ :confidential,
6
+ :boolean,
7
+ null: false,
8
+ default: true # maintaining backwards compatibility: require secrets
9
+ )
10
+ end
11
+ end
@@ -6,6 +6,7 @@ class CreateDoorkeeperTables < ActiveRecord::Migration<%= migration_version %>
6
6
  t.string :secret, null: false
7
7
  t.text :redirect_uri, null: false
8
8
  t.string :scopes, null: false, default: ''
9
+ t.boolean :confidential, null: false, default: true
9
10
  t.timestamps null: false
10
11
  end
11
12
 
@@ -54,7 +54,7 @@ describe Doorkeeper::AuthorizationsController, 'implicit grant flow' do
54
54
  end
55
55
 
56
56
  it 'includes token type in fragment' do
57
- expect(response.query_params['token_type']).to eq('bearer')
57
+ expect(response.query_params['token_type']).to eq('Bearer')
58
58
  end
59
59
 
60
60
  it 'includes token expiration in fragment' do
@@ -164,6 +164,38 @@ describe Doorkeeper::AuthorizationsController, 'implicit grant flow' do
164
164
  it 'should not issue a token' do
165
165
  expect(Doorkeeper::AccessToken.count).to be 0
166
166
  end
167
+
168
+ context 'with opt_out_native_route_change' do
169
+ around(:each) do |example|
170
+ Doorkeeper.configure do
171
+ orm DOORKEEPER_ORM
172
+ opt_out_native_route_change
173
+ end
174
+
175
+ Rails.application.reload_routes!
176
+
177
+ example.run
178
+
179
+ Doorkeeper.configure do
180
+ orm DOORKEEPER_ORM
181
+ end
182
+
183
+ Rails.application.reload_routes!
184
+ end
185
+
186
+ it 'should redirect immediately' do
187
+ expect(response).to be_redirect
188
+ expect(response.location).to match(/oauth\/authorize\/#{Doorkeeper::AccessGrant.first.token}/)
189
+ end
190
+
191
+ it 'should issue a grant' do
192
+ expect(Doorkeeper::AccessGrant.count).to be 1
193
+ end
194
+
195
+ it 'should not issue a token' do
196
+ expect(Doorkeeper::AccessToken.count).to be 0
197
+ end
198
+ end
167
199
  end
168
200
 
169
201
  describe 'GET #new with skip_authorization true' do
@@ -184,7 +216,7 @@ describe Doorkeeper::AuthorizationsController, 'implicit grant flow' do
184
216
  end
185
217
 
186
218
  it 'includes token type in fragment' do
187
- expect(response.query_params['token_type']).to eq('bearer')
219
+ expect(response.query_params['token_type']).to eq('Bearer')
188
220
  end
189
221
 
190
222
  it 'includes token expiration in fragment' do
@@ -59,15 +59,67 @@ describe Doorkeeper::TokensController do
59
59
  end
60
60
  end
61
61
 
62
- describe 'when revoke authorization has failed' do
63
- # http://tools.ietf.org/html/rfc7009#section-2.2
64
- it 'returns no error response' do
65
- token = double(:token, authorize: false, application_id?: true)
66
- allow(controller).to receive(:token) { token }
62
+ # http://tools.ietf.org/html/rfc7009#section-2.2
63
+ describe 'revoking tokens' do
64
+ let(:client) { FactoryBot.create(:application) }
65
+ let(:access_token) { FactoryBot.create(:access_token, application: client) }
66
+
67
+ before(:each) do
68
+ allow(controller).to receive(:token) { access_token }
69
+ end
70
+
71
+ context 'when associated app is public' do
72
+ let(:client) { FactoryBot.create(:application, confidential: false) }
73
+
74
+ it 'returns 200' do
75
+ post :revoke
76
+
77
+ expect(response.status).to eq 200
78
+ end
79
+
80
+ it 'revokes the access token' do
81
+ post :revoke
82
+
83
+ expect(access_token.reload).to have_attributes(revoked?: true)
84
+ end
85
+ end
86
+
87
+ context 'when associated app is confidential' do
88
+ let(:client) { FactoryBot.create(:application, confidential: true) }
89
+ let(:oauth_client) { Doorkeeper::OAuth::Client.new(client) }
67
90
 
68
- post :revoke
91
+ before(:each) do
92
+ allow_any_instance_of(Doorkeeper::Server).to receive(:client) { oauth_client }
93
+ end
94
+
95
+ it 'returns 200' do
96
+ post :revoke
97
+
98
+ expect(response.status).to eq 200
99
+ end
100
+
101
+ it 'revokes the access token' do
102
+ post :revoke
103
+
104
+ expect(access_token.reload).to have_attributes(revoked?: true)
105
+ end
106
+
107
+ context 'when authorization fails' do
108
+ let(:some_other_client) { FactoryBot.create(:application, confidential: true) }
109
+ let(:oauth_client) { Doorkeeper::OAuth::Client.new(some_other_client) }
110
+
111
+ it 'returns 200' do
112
+ post :revoke
69
113
 
70
- expect(response.status).to eq 200
114
+ expect(response.status).to eq 200
115
+ end
116
+
117
+ it 'does not revoke the access token' do
118
+ post :revoke
119
+
120
+ expect(access_token.reload).to have_attributes(revoked?: false)
121
+ end
122
+ end
71
123
  end
72
124
  end
73
125
 
@@ -29,6 +29,11 @@ Doorkeeper.configure do
29
29
  # Issue access tokens with refresh token (disabled by default)
30
30
  use_refresh_token
31
31
 
32
+ # Opt out of breaking api change to the native authorization code flow. Opting out sets the authorization
33
+ # code response route for native redirect uris to oauth/authorize/<code>. The default is oauth/authorize/native?code=<code>.
34
+ # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1143
35
+ # opt_out_native_route_change
36
+
32
37
  # Provide support for an owner to be assigned to each registered application (disabled by default)
33
38
  # Optional parameter confirmation: true (default false) if you want to enforce ownership of
34
39
  # a registered application
@@ -1,4 +1,6 @@
1
- class CreateUsers < ActiveRecord::Migration
1
+ # frozen_string_literal: true
2
+
3
+ class CreateUsers < ActiveRecord::Migration[4.2]
2
4
  def change
3
5
  create_table :users do |t|
4
6
  t.string :name
@@ -1,4 +1,6 @@
1
- class AddPasswordToUsers < ActiveRecord::Migration
1
+ # frozen_string_literal: true
2
+
3
+ class AddPasswordToUsers < ActiveRecord::Migration[4.2]
2
4
  def change
3
5
  add_column :users, :password, :string
4
6
  end
@@ -1,4 +1,6 @@
1
- class CreateDoorkeeperTables < ActiveRecord::Migration
1
+ # frozen_string_literal: true
2
+
3
+ class CreateDoorkeeperTables < ActiveRecord::Migration[4.2]
2
4
  def change
3
5
  create_table :oauth_applications do |t|
4
6
  t.string :name, null: false
@@ -1,4 +1,6 @@
1
- class AddOwnerToApplication < ActiveRecord::Migration
1
+ # frozen_string_literal: true
2
+
3
+ class AddOwnerToApplication < ActiveRecord::Migration[4.2]
2
4
  def change
3
5
  add_column :oauth_applications, :owner_id, :integer, null: true
4
6
  add_column :oauth_applications, :owner_type, :string, null: true
@@ -1,4 +1,6 @@
1
- class AddPreviousRefreshTokenToAccessTokens < ActiveRecord::Migration
1
+ # frozen_string_literal: true
2
+
3
+ class AddPreviousRefreshTokenToAccessTokens < ActiveRecord::Migration[4.2]
2
4
  def change
3
5
  add_column(
4
6
  :oauth_access_tokens,
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddConfidentialToApplication < ActiveRecord::Migration[5.1]
4
+ def change
5
+ add_column(
6
+ :oauth_applications,
7
+ :confidential,
8
+ :boolean,
9
+ null: false,
10
+ default: true # maintaining backwards compatibility: require secrets
11
+ )
12
+ end
13
+ end
@@ -11,7 +11,7 @@
11
11
  #
12
12
  # It's strongly recommended that you check this file into your version control system.
13
13
 
14
- ActiveRecord::Schema.define(version: 20160320211015) do
14
+ ActiveRecord::Schema.define(version: 20180210183654) do
15
15
 
16
16
  create_table "oauth_access_grants", force: :cascade do |t|
17
17
  t.integer "resource_owner_id", null: false
@@ -52,6 +52,7 @@ ActiveRecord::Schema.define(version: 20160320211015) do
52
52
  t.datetime "updated_at"
53
53
  t.integer "owner_id"
54
54
  t.string "owner_type"
55
+ t.boolean "confidential", default: true, null: false
55
56
  end
56
57
 
57
58
  add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type"
@@ -162,6 +162,31 @@ describe Doorkeeper, 'configuration' do
162
162
  end
163
163
  end
164
164
 
165
+ describe 'opt_out_native_route_change' do
166
+ around(:each) do |example|
167
+ Doorkeeper.configure do
168
+ orm DOORKEEPER_ORM
169
+ opt_out_native_route_change
170
+ end
171
+
172
+ Rails.application.reload_routes!
173
+
174
+ subject { Doorkeeper.configuration }
175
+
176
+ example.run
177
+
178
+ Doorkeeper.configure do
179
+ orm DOORKEEPER_ORM
180
+ end
181
+
182
+ Rails.application.reload_routes!
183
+ end
184
+
185
+ it 'sets the native authorization code route /:code' do
186
+ expect(subject.native_authorization_code_route).to eq('/:code')
187
+ end
188
+ end
189
+
165
190
  describe 'client_credentials' do
166
191
  it 'has defaults order' do
167
192
  expect(subject.client_credentials_methods).to eq([:from_basic, :from_params])
@@ -104,5 +104,20 @@ module Doorkeeper::OAuth
104
104
  expect(subject.error).to eq(:invalid_grant)
105
105
  end
106
106
  end
107
+
108
+ context "when redirect_uri is the native one" do
109
+ let(:redirect_uri) { 'urn:ietf:wg:oauth:2.0:oob' }
110
+
111
+ it "invalidates when redirect_uri of the grant is not native" do
112
+ subject.validate
113
+ expect(subject.error).to eq(:invalid_grant)
114
+ end
115
+
116
+ it "validates when redirect_uri of the grant is also native" do
117
+ allow(grant).to receive(:redirect_uri) { redirect_uri }
118
+ subject.validate
119
+ expect(subject.error).to eq(nil)
120
+ end
121
+ end
107
122
  end
108
123
  end
@@ -7,9 +7,11 @@ class Doorkeeper::OAuth::Client
7
7
  let(:client_id) { 'some-uid' }
8
8
  let(:client_secret) { 'some-secret' }
9
9
 
10
- it 'is blank when any of the credentials is blank' do
10
+ it 'is blank when the uid in credentials is blank' do
11
+ expect(Credentials.new(nil, nil)).to be_blank
11
12
  expect(Credentials.new(nil, 'something')).to be_blank
12
- expect(Credentials.new('something', nil)).to be_blank
13
+ expect(Credentials.new('something', nil)).to be_present
14
+ expect(Credentials.new('something', 'something')).to be_present
13
15
  end
14
16
 
15
17
  describe :from_request do
@@ -5,6 +5,11 @@ require 'doorkeeper/oauth/helpers/uri_checker'
5
5
  module Doorkeeper::OAuth::Helpers
6
6
  describe URIChecker do
7
7
  describe '.valid?' do
8
+ it 'is valid for native uris' do
9
+ uri = 'urn:ietf:wg:oauth:2.0:oob'
10
+ expect(URIChecker.valid?(uri)).to be_truthy
11
+ end
12
+
8
13
  it 'is valid for valid uris' do
9
14
  uri = 'http://app.co'
10
15
  expect(URIChecker.valid?(uri)).to be_truthy
@@ -123,14 +123,19 @@ module Doorkeeper::OAuth
123
123
  expect(subject.scopes).to eq(Scopes.from_string('default'))
124
124
  end
125
125
 
126
- it 'accepts test uri' do
127
- subject.redirect_uri = 'urn:ietf:wg:oauth:2.0:oob'
128
- expect(subject).to be_authorizable
129
- end
126
+ context 'with native redirect uri' do
127
+ let(:native_redirect_uri) { 'urn:ietf:wg:oauth:2.0:oob' }
130
128
 
131
- it 'matches the redirect uri against client\'s one' do
132
- subject.redirect_uri = 'http://nothesame.com'
133
- expect(subject).not_to be_authorizable
129
+ it 'accepts redirect_uri when it matches with the client' do
130
+ subject.redirect_uri = native_redirect_uri
131
+ allow(subject.client).to receive(:redirect_uri) { native_redirect_uri }
132
+ expect(subject).to be_authorizable
133
+ end
134
+
135
+ it 'invalidates redirect_uri when it does\'n match with the client' do
136
+ subject.redirect_uri = native_redirect_uri
137
+ expect(subject).not_to be_authorizable
138
+ end
134
139
  end
135
140
 
136
141
  it 'stores the state' do
@@ -49,6 +49,11 @@ module Doorkeeper
49
49
  expect(new_application).not_to be_valid
50
50
  end
51
51
 
52
+ it 'is invalid without determining confidentiality' do
53
+ new_application.confidential = nil
54
+ expect(new_application).not_to be_valid
55
+ end
56
+
52
57
  it 'generates uid on create' do
53
58
  expect(new_application.uid).to be_nil
54
59
  new_application.save
@@ -201,11 +206,97 @@ module Doorkeeper
201
206
  end
202
207
  end
203
208
 
204
- describe :authenticate do
205
- it 'finds the application via uid/secret' do
206
- app = FactoryBot.create :application
207
- authenticated = Application.by_uid_and_secret(app.uid, app.secret)
208
- expect(authenticated).to eq(app)
209
+ describe :by_uid_and_secret do
210
+ context "when application is private/confidential" do
211
+ it "finds the application via uid/secret" do
212
+ app = FactoryBot.create :application
213
+ authenticated = Application.by_uid_and_secret(app.uid, app.secret)
214
+ expect(authenticated).to eq(app)
215
+ end
216
+ context "when secret is wrong" do
217
+ it "should not find the application" do
218
+ app = FactoryBot.create :application
219
+ authenticated = Application.by_uid_and_secret(app.uid, 'bad')
220
+ expect(authenticated).to eq(nil)
221
+ end
222
+ end
223
+ end
224
+
225
+ context "when application is public/non-confidential" do
226
+ context "when secret is blank" do
227
+ it "should find the application" do
228
+ app = FactoryBot.create :application, confidential: false
229
+ authenticated = Application.by_uid_and_secret(app.uid, nil)
230
+ expect(authenticated).to eq(app)
231
+ end
232
+ end
233
+ context "when secret is wrong" do
234
+ it "should not find the application" do
235
+ app = FactoryBot.create :application, confidential: false
236
+ authenticated = Application.by_uid_and_secret(app.uid, 'bad')
237
+ expect(authenticated).to eq(nil)
238
+ end
239
+ end
240
+ end
241
+ end
242
+
243
+ describe :confidential? do
244
+ subject { FactoryBot.create(:application, confidential: confidential).confidential? }
245
+
246
+ context 'when application is private/confidential' do
247
+ let(:confidential) { true }
248
+ it { expect(subject).to eq(true) }
249
+ end
250
+
251
+ context 'when application is public/non-confidential' do
252
+ let(:confidential) { false }
253
+ it { expect(subject).to eq(false) }
254
+ end
255
+ end
256
+
257
+ describe :confidential do
258
+ subject { FactoryBot.create(:application, confidential: confidential).confidential }
259
+
260
+ context 'when application is private/confidential' do
261
+ let(:confidential) { true }
262
+ it { expect(subject).to eq(true) }
263
+ end
264
+
265
+ context 'when application is public/non-confidential' do
266
+ let(:confidential) { false }
267
+ it { expect(subject).to eq(false) }
268
+ end
269
+
270
+ context 'when the application does not support confidentiality' do
271
+ let(:confidential) { false }
272
+
273
+ before { allow(Application).to receive(:supports_confidentiality?).and_return(false) }
274
+
275
+ it 'warns of the CVE' do
276
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(
277
+ 'You are susceptible to security bug ' \
278
+ 'CVE-2018-1000211. Please follow instructions outlined in ' \
279
+ 'Doorkeeper::CVE_2018_1000211_WARNING'
280
+ )
281
+ Application.new.confidential
282
+ end
283
+
284
+ it { expect(subject).to eq(true) }
285
+ end
286
+ end
287
+
288
+ describe :supports_confidentiality? do
289
+ context 'when no column' do
290
+ it 'returns false' do
291
+ expect(Application).to receive(:column_names).and_return(%w[foo bar])
292
+ expect(Application.supports_confidentiality?).to eq(false)
293
+ end
294
+ end
295
+ context 'when column' do
296
+ it 'returns true' do
297
+ expect(Application).to receive(:column_names).and_return(%w[foo bar confidential])
298
+ expect(Application.supports_confidentiality?).to eq(true)
299
+ end
209
300
  end
210
301
  end
211
302
  end
@@ -53,7 +53,7 @@ feature 'Authorization Code Flow' do
53
53
  should_not_have_json 'error'
54
54
 
55
55
  should_have_json 'access_token', Doorkeeper::AccessToken.first.token
56
- should_have_json 'token_type', 'bearer'
56
+ should_have_json 'token_type', 'Bearer'
57
57
  should_have_json_within 'expires_in', Doorkeeper::AccessToken.first.expires_in, 1
58
58
  end
59
59
 
@@ -10,46 +10,89 @@ describe 'Resource Owner Password Credentials Flow not set up' do
10
10
  it 'doesn\'t issue new token' do
11
11
  expect do
12
12
  post password_token_endpoint_url(client: @client, resource_owner: @resource_owner)
13
- end.to_not change { Doorkeeper::AccessToken.count }
13
+ end.to_not(change { Doorkeeper::AccessToken.count })
14
14
  end
15
15
  end
16
16
  end
17
17
 
18
18
  describe 'Resource Owner Password Credentials Flow' do
19
+ let(:client_attributes) { {} }
20
+
19
21
  before do
20
22
  config_is_set(:grant_flows, ["password"])
21
23
  config_is_set(:resource_owner_from_credentials) { User.authenticate! params[:username], params[:password] }
22
- client_exists
24
+ client_exists(client_attributes)
23
25
  create_resource_owner
24
26
  end
25
27
 
26
28
  context 'with valid user credentials' do
27
- it 'should issue new token with confidential client' do
28
- expect do
29
- post password_token_endpoint_url(client: @client, resource_owner: @resource_owner)
30
- end.to change { Doorkeeper::AccessToken.count }.by(1)
29
+ context "with non-confidential/public client" do
30
+ let(:client_attributes) { { confidential: false } }
31
31
 
32
- token = Doorkeeper::AccessToken.first
32
+ context "when client_secret absent" do
33
+ it "should issue new token" do
34
+ expect do
35
+ post password_token_endpoint_url(client_id: @client.uid, resource_owner: @resource_owner)
36
+ end.to change { Doorkeeper::AccessToken.count }.by(1)
33
37
 
34
- expect(token.application_id).to eq @client.id
35
- should_have_json 'access_token', token.token
38
+ token = Doorkeeper::AccessToken.first
39
+
40
+ expect(token.application_id).to eq @client.id
41
+ should_have_json 'access_token', token.token
42
+ end
43
+ end
44
+
45
+ context "when client_secret present" do
46
+ it "should issue new token" do
47
+ expect do
48
+ post password_token_endpoint_url(client: @client, resource_owner: @resource_owner)
49
+ end.to change { Doorkeeper::AccessToken.count }.by(1)
50
+
51
+ token = Doorkeeper::AccessToken.first
52
+
53
+ expect(token.application_id).to eq @client.id
54
+ should_have_json 'access_token', token.token
55
+ end
56
+
57
+ context "when client_secret incorrect" do
58
+ it "should not issue new token" do
59
+ expect do
60
+ post password_token_endpoint_url(client_id: @client.uid, client_secret: 'foobar', resource_owner: @resource_owner)
61
+ end.not_to(change { Doorkeeper::AccessToken.count })
62
+
63
+ expect(response).not_to be_ok
64
+ end
65
+ end
66
+ end
36
67
  end
37
68
 
38
- it 'should issue new token with public client (only client_id present)' do
39
- expect do
40
- post password_token_endpoint_url(client_id: @client.uid, resource_owner: @resource_owner)
41
- end.to change { Doorkeeper::AccessToken.count }.by(1)
69
+ context "with confidential/private client" do
70
+ it "should issue new token" do
71
+ expect do
72
+ post password_token_endpoint_url(client: @client, resource_owner: @resource_owner)
73
+ end.to change { Doorkeeper::AccessToken.count }.by(1)
42
74
 
43
- token = Doorkeeper::AccessToken.first
75
+ token = Doorkeeper::AccessToken.first
44
76
 
45
- expect(token.application_id).to eq @client.id
46
- should_have_json 'access_token', token.token
77
+ expect(token.application_id).to eq @client.id
78
+ should_have_json 'access_token', token.token
79
+ end
80
+
81
+ context "when client_secret absent" do
82
+ it "should not issue new token" do
83
+ expect do
84
+ post password_token_endpoint_url(client_id: @client.uid, resource_owner: @resource_owner)
85
+ end.not_to(change { Doorkeeper::AccessToken.count })
86
+
87
+ expect(response).not_to be_ok
88
+ end
89
+ end
47
90
  end
48
91
 
49
92
  it 'should issue new token without client credentials' do
50
93
  expect do
51
94
  post password_token_endpoint_url(resource_owner: @resource_owner)
52
- end.to change { Doorkeeper::AccessToken.count }.by(1)
95
+ end.to(change { Doorkeeper::AccessToken.count }.by(1))
53
96
 
54
97
  token = Doorkeeper::AccessToken.first
55
98
 
@@ -124,13 +167,13 @@ describe 'Resource Owner Password Credentials Flow' do
124
167
  post password_token_endpoint_url(client: @client,
125
168
  resource_owner_username: @resource_owner.name,
126
169
  resource_owner_password: 'wrongpassword')
127
- end.to_not change { Doorkeeper::AccessToken.count }
170
+ end.to_not(change { Doorkeeper::AccessToken.count })
128
171
  end
129
172
 
130
173
  it 'should not issue new token without credentials' do
131
174
  expect do
132
175
  post password_token_endpoint_url(client: @client)
133
- end.to_not change { Doorkeeper::AccessToken.count }
176
+ end.to_not(change { Doorkeeper::AccessToken.count })
134
177
  end
135
178
  end
136
179
 
@@ -140,7 +183,7 @@ describe 'Resource Owner Password Credentials Flow' do
140
183
  post password_token_endpoint_url(client_id: @client.uid,
141
184
  client_secret: 'bad_secret',
142
185
  resource_owner: @resource_owner)
143
- end.to_not change { Doorkeeper::AccessToken.count }
186
+ end.to_not(change { Doorkeeper::AccessToken.count })
144
187
  end
145
188
  end
146
189
 
@@ -148,7 +191,7 @@ describe 'Resource Owner Password Credentials Flow' do
148
191
  it 'should not issue new token with bad client id' do
149
192
  expect do
150
193
  post password_token_endpoint_url(client_id: 'bad_id', resource_owner: @resource_owner)
151
- end.to_not change { Doorkeeper::AccessToken.count }
194
+ end.to_not(change { Doorkeeper::AccessToken.count })
152
195
  end
153
196
  end
154
197
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: doorkeeper
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.2
4
+ version: 4.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Felipe Elias Philipp
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2018-03-28 00:00:00.000000000 Z
14
+ date: 2018-09-19 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: railties
@@ -259,11 +259,13 @@ files:
259
259
  - lib/doorkeeper/server.rb
260
260
  - lib/doorkeeper/validations.rb
261
261
  - lib/doorkeeper/version.rb
262
+ - lib/generators/doorkeeper/add_client_confidentiality_generator.rb
262
263
  - lib/generators/doorkeeper/application_owner_generator.rb
263
264
  - lib/generators/doorkeeper/install_generator.rb
264
265
  - lib/generators/doorkeeper/migration_generator.rb
265
266
  - lib/generators/doorkeeper/previous_refresh_token_generator.rb
266
267
  - lib/generators/doorkeeper/templates/README
268
+ - lib/generators/doorkeeper/templates/add_confidential_to_application_migration.rb.erb
267
269
  - lib/generators/doorkeeper/templates/add_owner_to_application_migration.rb.erb
268
270
  - lib/generators/doorkeeper/templates/add_previous_refresh_token_to_access_tokens.rb.erb
269
271
  - lib/generators/doorkeeper/templates/initializer.rb
@@ -307,6 +309,7 @@ files:
307
309
  - spec/dummy/db/migrate/20151223192035_create_doorkeeper_tables.rb
308
310
  - spec/dummy/db/migrate/20151223200000_add_owner_to_application.rb
309
311
  - spec/dummy/db/migrate/20160320211015_add_previous_refresh_token_to_access_tokens.rb
312
+ - spec/dummy/db/migrate/20180210183654_add_confidential_to_application.rb
310
313
  - spec/dummy/db/schema.rb
311
314
  - spec/dummy/public/404.html
312
315
  - spec/dummy/public/422.html
@@ -397,7 +400,25 @@ homepage: https://github.com/doorkeeper-gem/doorkeeper
397
400
  licenses:
398
401
  - MIT
399
402
  metadata: {}
400
- post_install_message:
403
+ post_install_message: |2+
404
+
405
+
406
+ WARNING: This is a security release that addresses token revocation not working for public apps (CVE-2018-1000211)
407
+
408
+ There is no breaking change in this release, however to take advantage of the security fix you must:
409
+
410
+ 1. Run `rails generate doorkeeper:add_client_confidentiality` for the migration
411
+ 2. Review your OAuth apps and determine which ones exclusively use public grant flows (eg implicit)
412
+ 3. Update their `confidential` column to `false` for those public apps
413
+
414
+ This is a backported security release.
415
+
416
+ For more information:
417
+
418
+ * https://github.com/doorkeeper-gem/doorkeeper/pull/1119
419
+ * https://github.com/doorkeeper-gem/doorkeeper/issues/891
420
+
421
+
401
422
  rdoc_options: []
402
423
  require_paths:
403
424
  - lib
@@ -456,6 +477,7 @@ test_files:
456
477
  - spec/dummy/db/migrate/20151223192035_create_doorkeeper_tables.rb
457
478
  - spec/dummy/db/migrate/20151223200000_add_owner_to_application.rb
458
479
  - spec/dummy/db/migrate/20160320211015_add_previous_refresh_token_to_access_tokens.rb
480
+ - spec/dummy/db/migrate/20180210183654_add_confidential_to_application.rb
459
481
  - spec/dummy/db/schema.rb
460
482
  - spec/dummy/public/404.html
461
483
  - spec/dummy/public/422.html