doorkeeper 5.1.2 → 5.2.0.rc1

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +812 -0
  3. data/CONTRIBUTING.md +4 -9
  4. data/Dangerfile +1 -1
  5. data/Gemfile +2 -1
  6. data/NEWS.md +1 -819
  7. data/README.md +2 -2
  8. data/RELEASING.md +6 -5
  9. data/app/controllers/doorkeeper/applications_controller.rb +5 -3
  10. data/app/controllers/doorkeeper/authorized_applications_controller.rb +1 -1
  11. data/app/controllers/doorkeeper/tokens_controller.rb +18 -8
  12. data/app/validators/redirect_uri_validator.rb +19 -9
  13. data/app/views/doorkeeper/applications/_form.html.erb +0 -6
  14. data/app/views/doorkeeper/applications/show.html.erb +1 -1
  15. data/config/locales/en.yml +3 -1
  16. data/doorkeeper.gemspec +1 -1
  17. data/gemfiles/rails_5_0.gemfile +1 -0
  18. data/gemfiles/rails_5_1.gemfile +1 -0
  19. data/gemfiles/rails_5_2.gemfile +1 -0
  20. data/gemfiles/rails_6_0.gemfile +2 -1
  21. data/gemfiles/rails_master.gemfile +1 -0
  22. data/lib/doorkeeper.rb +3 -0
  23. data/lib/doorkeeper/config.rb +30 -3
  24. data/lib/doorkeeper/config/option.rb +13 -7
  25. data/lib/doorkeeper/grape/helpers.rb +5 -1
  26. data/lib/doorkeeper/helpers/controller.rb +16 -3
  27. data/lib/doorkeeper/oauth/authorization/code.rb +10 -8
  28. data/lib/doorkeeper/oauth/authorization/token.rb +1 -1
  29. data/lib/doorkeeper/oauth/code_response.rb +2 -2
  30. data/lib/doorkeeper/oauth/error_response.rb +1 -1
  31. data/lib/doorkeeper/oauth/helpers/uri_checker.rb +18 -4
  32. data/lib/doorkeeper/oauth/nonstandard.rb +39 -0
  33. data/lib/doorkeeper/oauth/refresh_token_request.rb +8 -8
  34. data/lib/doorkeeper/oauth/token_introspection.rb +13 -12
  35. data/lib/doorkeeper/orm/active_record.rb +17 -1
  36. data/lib/doorkeeper/orm/active_record/access_grant.rb +1 -1
  37. data/lib/doorkeeper/orm/active_record/access_token.rb +2 -2
  38. data/lib/doorkeeper/orm/active_record/application.rb +5 -65
  39. data/lib/doorkeeper/stale_records_cleaner.rb +6 -2
  40. data/lib/doorkeeper/version.rb +3 -3
  41. data/lib/generators/doorkeeper/previous_refresh_token_generator.rb +6 -6
  42. data/lib/generators/doorkeeper/templates/initializer.rb +41 -9
  43. data/lib/generators/doorkeeper/templates/migration.rb.erb +3 -0
  44. data/spec/controllers/applications_controller_spec.rb +93 -0
  45. data/spec/controllers/protected_resources_controller_spec.rb +3 -3
  46. data/spec/controllers/tokens_controller_spec.rb +71 -3
  47. data/spec/dummy/config/application.rb +3 -1
  48. data/spec/dummy/config/initializers/doorkeeper.rb +27 -9
  49. data/spec/lib/config_spec.rb +11 -0
  50. data/spec/lib/oauth/helpers/uri_checker_spec.rb +17 -2
  51. data/spec/lib/oauth/pre_authorization_spec.rb +0 -15
  52. data/spec/models/doorkeeper/application_spec.rb +268 -373
  53. data/spec/requests/flows/authorization_code_spec.rb +16 -4
  54. data/spec/requests/flows/revoke_token_spec.rb +19 -11
  55. data/spec/support/doorkeeper_rspec.rb +1 -1
  56. data/spec/validators/redirect_uri_validator_spec.rb +39 -14
  57. metadata +7 -15
  58. data/.coveralls.yml +0 -1
  59. data/.github/ISSUE_TEMPLATE.md +0 -25
  60. data/.github/PULL_REQUEST_TEMPLATE.md +0 -17
  61. data/.gitignore +0 -20
  62. data/.gitlab-ci.yml +0 -16
  63. data/.hound.yml +0 -3
  64. data/.rspec +0 -1
  65. data/.rubocop.yml +0 -50
  66. data/.travis.yml +0 -35
@@ -36,6 +36,9 @@ class CreateDoorkeeperTables < ActiveRecord::Migration<%= migration_version %>
36
36
 
37
37
  create_table :oauth_access_tokens do |t|
38
38
  t.references :resource_owner, index: true
39
+
40
+ # Remove `null: false` if you are planning to use Password
41
+ # Credentials Grant flow that doesn't require an application.
39
42
  t.references :application, null: false
40
43
 
41
44
  # If you use a custom token generator you may need to change this column
@@ -26,6 +26,10 @@ module Doorkeeper
26
26
 
27
27
  expect(json_response).to include("id", "name", "uid", "secret", "redirect_uri", "scopes")
28
28
 
29
+ application = Application.last
30
+ secret_from_response = json_response["secret"]
31
+ expect(application.secret_matches?(secret_from_response)).to be_truthy
32
+
29
33
  expect(json_response["name"]).to eq("Example")
30
34
  expect(json_response["redirect_uri"]).to eq("https://example.com")
31
35
  end
@@ -44,6 +48,21 @@ module Doorkeeper
44
48
  expect(json_response).to include("errors")
45
49
  end
46
50
 
51
+ it "returns validations on wrong create params (unspecified scheme)" do
52
+ expect do
53
+ post :create, params: {
54
+ doorkeeper_application: {
55
+ name: "Example",
56
+ redirect_uri: "app.com:80",
57
+ }, format: :json,
58
+ }
59
+ end.not_to(change { Doorkeeper::Application.count })
60
+
61
+ expect(response).to have_http_status(422)
62
+
63
+ expect(json_response).to include("errors")
64
+ end
65
+
47
66
  it "returns application info" do
48
67
  application = FactoryBot.create(:application, name: "Change me")
49
68
 
@@ -121,6 +140,72 @@ module Doorkeeper
121
140
  end
122
141
 
123
142
  context "when admin is authenticated" do
143
+ context "when application secrets are hashed" do
144
+ before do
145
+ allow(Doorkeeper.configuration).to receive(:application_secret_strategy).and_return(Doorkeeper::SecretStoring::Sha256Hash)
146
+ end
147
+
148
+ it "shows the application secret after creating a new application" do
149
+ expect do
150
+ post :create, params: {
151
+ doorkeeper_application: {
152
+ name: "Example",
153
+ redirect_uri: "https://example.com",
154
+ },
155
+ }
156
+ end.to change { Doorkeeper::Application.count }.by(1)
157
+
158
+ application = Application.last
159
+
160
+ secret_from_flash = flash[:application_secret]
161
+ expect(secret_from_flash).not_to be_empty
162
+ expect(application.secret_matches?(secret_from_flash)).to be_truthy
163
+ expect(response).to redirect_to(controller.main_app.oauth_application_url(application.id))
164
+
165
+ get :show, params: { id: application.id, format: :html }
166
+
167
+ # We don't know the application secret here (because its hashed) so we can not assert its text on the page
168
+ # Instead, we read it from the page and then check if it matches the application secret
169
+ code_element = %r{<code.*id="secret".*>(.*)<\/code>}.match(response.body)
170
+ secret_from_page = code_element[1]
171
+
172
+ expect(response.body).to have_selector("code#application_id", text: application.uid)
173
+ expect(response.body).to have_selector("code#secret")
174
+ expect(secret_from_page).not_to be_empty
175
+ expect(application.secret_matches?(secret_from_page)).to be_truthy
176
+ end
177
+
178
+ it "does not show an application secret when application did already exist" do
179
+ application = FactoryBot.create(:application)
180
+ get :show, params: { id: application.id, format: :html }
181
+
182
+ expect(response.body).to have_selector("code#application_id", text: application.uid)
183
+ expect(response.body).to have_selector("code#secret", text: "")
184
+ end
185
+
186
+ it "returns the application details in a json response" do
187
+ expect do
188
+ post :create, params: {
189
+ doorkeeper_application: {
190
+ name: "Example",
191
+ redirect_uri: "https://example.com",
192
+ }, format: :json,
193
+ }
194
+ end.to(change { Doorkeeper::Application.count })
195
+
196
+ expect(response).to be_successful
197
+
198
+ expect(json_response).to include("id", "name", "uid", "secret", "redirect_uri", "scopes")
199
+
200
+ application = Application.last
201
+ secret_from_response = json_response["secret"]
202
+ expect(application.secret_matches?(secret_from_response)).to be_truthy
203
+
204
+ expect(json_response["name"]).to eq("Example")
205
+ expect(json_response["redirect_uri"]).to eq("https://example.com")
206
+ end
207
+ end
208
+
124
209
  render_views
125
210
 
126
211
  before do
@@ -151,6 +236,14 @@ module Doorkeeper
151
236
  expect(response).to be_redirect
152
237
  end
153
238
 
239
+ it "shows application details" do
240
+ application = FactoryBot.create(:application)
241
+ get :show, params: { id: application.id, format: :html }
242
+
243
+ expect(response.body).to have_selector("code#application_id", text: application.uid)
244
+ expect(response.body).to have_selector("code#secret", text: application.plaintext_secret)
245
+ end
246
+
154
247
  it "does not allow mass assignment of uid or secret" do
155
248
  application = FactoryBot.create(:application)
156
249
  put :update, params: {
@@ -166,7 +166,7 @@ describe "doorkeeper authorize filter" do
166
166
  it "it renders a custom JSON response", token: :invalid do
167
167
  get :index, params: { access_token: token_string }
168
168
  expect(response.status).to eq 401
169
- expect(response.content_type).to match(/application\/json/)
169
+ expect(response.content_type).to eq("application/json")
170
170
  expect(response.header["WWW-Authenticate"]).to match(/^Bearer/)
171
171
 
172
172
  expect(json_response).not_to be_nil
@@ -196,7 +196,7 @@ describe "doorkeeper authorize filter" do
196
196
  it "it renders a custom text response", token: :invalid do
197
197
  get :index, params: { access_token: token_string }
198
198
  expect(response.status).to eq 401
199
- expect(response.content_type).to match(/text\/plain/)
199
+ expect(response.content_type).to eq("text/plain")
200
200
  expect(response.header["WWW-Authenticate"]).to match(/^Bearer/)
201
201
  expect(response.body).to eq("Unauthorized")
202
202
  end
@@ -246,7 +246,7 @@ describe "doorkeeper authorize filter" do
246
246
  it "renders a custom JSON response" do
247
247
  get :index, params: { access_token: token_string }
248
248
  expect(response.header).to_not include("WWW-Authenticate")
249
- expect(response.content_type).to match(/application\/json/)
249
+ expect(response.content_type).to eq("application/json")
250
250
  expect(response.status).to eq 403
251
251
 
252
252
  expect(json_response).not_to be_nil
@@ -102,10 +102,10 @@ describe Doorkeeper::TokensController do
102
102
  let(:some_other_client) { FactoryBot.create(:application, confidential: true) }
103
103
  let(:oauth_client) { Doorkeeper::OAuth::Client.new(some_other_client) }
104
104
 
105
- it "returns 200" do
105
+ it "returns 403" do
106
106
  post :revoke, params: { token: access_token.token }
107
107
 
108
- expect(response.status).to eq 200
108
+ expect(response.status).to eq 403
109
109
  end
110
110
 
111
111
  it "does not revoke the access token" do
@@ -147,7 +147,7 @@ describe Doorkeeper::TokensController do
147
147
  end
148
148
  end
149
149
 
150
- context "authorized using valid Client Authentication" do
150
+ context "authorized using Client Credentials of the client that token is issued to" do
151
151
  it "responds with full token introspection" do
152
152
  request.headers["Authorization"] = basic_auth_header_for_client(client)
153
153
 
@@ -159,6 +159,24 @@ describe Doorkeeper::TokensController do
159
159
  end
160
160
  end
161
161
 
162
+ context "configured token introspection disabled" do
163
+ before do
164
+ Doorkeeper.configure do
165
+ orm DOORKEEPER_ORM
166
+ allow_token_introspection false
167
+ end
168
+ end
169
+
170
+ it "responds with just active: false response" do
171
+ request.headers["Authorization"] = "Bearer #{access_token.token}"
172
+
173
+ post :introspect, params: { token: token_for_introspection.token }
174
+
175
+ should_have_json "active", false
176
+ expect(json_response).not_to include("client_id", "token_type", "exp", "iat")
177
+ end
178
+ end
179
+
162
180
  context "using custom introspection response" do
163
181
  before do
164
182
  Doorkeeper.configure do
@@ -212,6 +230,56 @@ describe Doorkeeper::TokensController do
212
230
  end
213
231
  end
214
232
 
233
+ context "introspection request authorized by a client and allow_token_introspection is true" do
234
+ let(:different_client) { FactoryBot.create(:application) }
235
+
236
+ before do
237
+ allow(Doorkeeper.configuration).to receive(:allow_token_introspection).and_return(proc do
238
+ true
239
+ end)
240
+ end
241
+
242
+ it "responds with full token introspection" do
243
+ request.headers["Authorization"] = basic_auth_header_for_client(different_client)
244
+
245
+ post :introspect, params: { token: token_for_introspection.token }
246
+
247
+ should_have_json "active", true
248
+ expect(json_response).to include("client_id", "token_type", "exp", "iat")
249
+ should_have_json "client_id", client.uid
250
+ end
251
+ end
252
+
253
+ context "allow_token_introspection requires authorized token with special scope" do
254
+ let(:access_token) { FactoryBot.create(:access_token, scopes: "introspection") }
255
+
256
+ before do
257
+ allow(Doorkeeper.configuration).to receive(:allow_token_introspection).and_return(proc do |_token, _client, authorized_token|
258
+ authorized_token.scopes.include?("introspection")
259
+ end)
260
+ end
261
+
262
+ it "responds with full token introspection if authorized token has introspection scope" do
263
+ request.headers["Authorization"] = "Bearer #{access_token.token}"
264
+
265
+ post :introspect, params: { token: token_for_introspection.token }
266
+
267
+ should_have_json "active", true
268
+ expect(json_response).to include("client_id", "token_type", "exp", "iat")
269
+ end
270
+
271
+ it "responds with just active status if authorized token doesn't have introspection scope" do
272
+ access_token.update(scopes: "read write")
273
+
274
+ request.headers["Authorization"] = "Bearer #{access_token.token}"
275
+
276
+ post :introspect, params: { token: token_for_introspection.token }
277
+
278
+ should_have_json "active", false
279
+ expect(json_response).not_to include("client_id", "token_type", "exp", "iat")
280
+ end
281
+ end
282
+
215
283
  context "authorized using invalid Bearer token" do
216
284
  let(:access_token) do
217
285
  FactoryBot.create(:access_token, application: client, revoked_at: 1.day.ago)
@@ -5,11 +5,13 @@ require "rails"
5
5
  %w[
6
6
  action_controller/railtie
7
7
  action_view/railtie
8
+ action_cable/engine
8
9
  sprockets/railtie
9
10
  ].each do |railtie|
10
11
  begin
11
12
  require railtie
12
- rescue LoadError
13
+ rescue LoadError => e
14
+ puts "Error loading '#{railtie}' (#{e.message})"
13
15
  end
14
16
  end
15
17
 
@@ -65,15 +65,6 @@ Doorkeeper.configure do
65
65
  # Check out the wiki for more information on customization
66
66
  # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param
67
67
 
68
- # Change the native redirect uri for client apps
69
- # When clients register with the following redirect uri, they won't be redirected to any server and
70
- # the authorization code will be displayed within the provider
71
- # The value can be any string. Use nil to disable this feature.
72
- # When disabled, clients must provide a valid URL
73
- # (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi)
74
- #
75
- # native_redirect_uri 'urn:ietf:wg:oauth:2.0:oob'
76
-
77
68
  # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled
78
69
  # by default in non-development environments). OAuth2 delegates security in
79
70
  # communication to the HTTPS protocol so it is wise to keep this enabled.
@@ -116,6 +107,33 @@ Doorkeeper.configure do
116
107
  # client.superapp? or resource_owner.admin?
117
108
  # end
118
109
 
110
+ # Implement constraints in case you use Client Credentials to authenticate
111
+ # the introspection endpoint.
112
+ # By default allow introspection if the introspected token belongs to authorized client,
113
+ # OR token doesn't belong to any client (public token). Otherwise disallow.
114
+ #
115
+ # Params:
116
+ # `token` - the token to be introspected (see Doorkeeper::AccessToken)
117
+ # `client` - the client application authorized for the endpoint (see Doorkeeper::Application)
118
+ #
119
+ # You can completely ignore it:
120
+ # allow_token_introspection do |_token, _client|
121
+ # false
122
+ # end
123
+ #
124
+ # Or you can define your custom check:
125
+ # Adding `protected_resource` boolean column to applications table
126
+ # to allow protected_resource client introspect the token of normal client.
127
+ # In this case, protected resource client must be confidential.
128
+ #
129
+ # allow_token_introspection do |token, client|
130
+ # if token.application
131
+ # token.application == client || client.protected_resource?
132
+ # else
133
+ # true
134
+ # end
135
+ # end
136
+
119
137
  # WWW-Authenticate Realm (default "Doorkeeper").
120
138
  realm "Doorkeeper"
121
139
  end
@@ -694,4 +694,15 @@ describe Doorkeeper, "configuration" do
694
694
  end
695
695
  end
696
696
  end
697
+
698
+ describe "options deprecation" do
699
+ it "prints a warning message when an option is deprecated" do
700
+ expect(Kernel).to receive(:warn).with(
701
+ "[DOORKEEPER] native_redirect_uri has been deprecated and will soon be removed"
702
+ )
703
+ Doorkeeper.configure do
704
+ native_redirect_uri "urn:ietf:wg:oauth:2.0:oob"
705
+ end
706
+ end
707
+ end
697
708
  end
@@ -40,13 +40,28 @@ module Doorkeeper::OAuth::Helpers
40
40
  expect(URIChecker.valid?(uri)).to be_falsey
41
41
  end
42
42
 
43
+ it "is invalid if localhost is resolved as as scheme (no scheme specified)" do
44
+ uri = "localhost:8080"
45
+ expect(URIChecker.valid?(uri)).to be_falsey
46
+ end
47
+
48
+ it "is invalid if scheme is missing #2" do
49
+ uri = "app.co:80"
50
+ expect(URIChecker.valid?(uri)).to be_falsey
51
+ end
52
+
43
53
  it "is invalid if is not an uri" do
44
54
  uri = " "
45
55
  expect(URIChecker.valid?(uri)).to be_falsey
46
56
  end
47
57
 
48
- it "is valid for native uris" do
49
- uri = "urn:ietf:wg:oauth:2.0:oob"
58
+ it "is valid for custom schemes" do
59
+ uri = "com.example.app:/test"
60
+ expect(URIChecker.valid?(uri)).to be_truthy
61
+ end
62
+
63
+ it "is valid for custom schemes with authority marker (common misconfiguration)" do
64
+ uri = "com.example.app://test"
50
65
  expect(URIChecker.valid?(uri)).to be_truthy
51
66
  end
52
67
  end
@@ -149,21 +149,6 @@ module Doorkeeper::OAuth
149
149
  expect(subject.scopes).to eq(Scopes.from_string("default"))
150
150
  end
151
151
 
152
- context "with native redirect uri" do
153
- let(:native_redirect_uri) { "urn:ietf:wg:oauth:2.0:oob" }
154
-
155
- it "accepts redirect_uri when it matches with the client" do
156
- subject.redirect_uri = native_redirect_uri
157
- allow(subject.client).to receive(:redirect_uri) { native_redirect_uri }
158
- expect(subject).to be_authorizable
159
- end
160
-
161
- it "invalidates redirect_uri when it does'n match with the client" do
162
- subject.redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
163
- expect(subject).not_to be_authorizable
164
- end
165
- end
166
-
167
152
  it "matches the redirect uri against client's one" do
168
153
  subject.redirect_uri = "http://nothesame.com"
169
154
  expect(subject).not_to be_authorizable
@@ -3,469 +3,364 @@
3
3
  require "spec_helper"
4
4
  require "bcrypt"
5
5
 
6
- describe Doorkeeper::Application do
7
- let(:require_owner) { Doorkeeper.configuration.instance_variable_set("@confirm_application_owner", true) }
8
- let(:unset_require_owner) { Doorkeeper.configuration.instance_variable_set("@confirm_application_owner", false) }
9
- let(:new_application) { FactoryBot.build(:application) }
6
+ module Doorkeeper
7
+ describe Application do
8
+ let(:clazz) { Doorkeeper::Application }
9
+ let(:require_owner) { Doorkeeper.configuration.instance_variable_set("@confirm_application_owner", true) }
10
+ let(:unset_require_owner) { Doorkeeper.configuration.instance_variable_set("@confirm_application_owner", false) }
11
+ let(:new_application) { FactoryBot.build(:application) }
10
12
 
11
- let(:uid) { SecureRandom.hex(8) }
12
- let(:secret) { SecureRandom.hex(8) }
13
+ let(:uid) { SecureRandom.hex(8) }
14
+ let(:secret) { SecureRandom.hex(8) }
13
15
 
14
- it "is invalid without a name" do
15
- new_application.name = nil
16
- expect(new_application).not_to be_valid
17
- end
18
-
19
- it "is invalid without determining confidentiality" do
20
- new_application.confidential = nil
21
- expect(new_application).not_to be_valid
22
- end
23
-
24
- it "generates uid on create" do
25
- expect(new_application.uid).to be_nil
26
- new_application.save
27
- expect(new_application.uid).not_to be_nil
28
- end
29
-
30
- it "generates uid on create if an empty string" do
31
- new_application.uid = ""
32
- new_application.save
33
- expect(new_application.uid).not_to be_blank
34
- end
35
-
36
- it "generates uid on create unless one is set" do
37
- new_application.uid = uid
38
- new_application.save
39
- expect(new_application.uid).to eq(uid)
40
- end
41
-
42
- it "is invalid without uid" do
43
- new_application.save
44
- new_application.uid = nil
45
- expect(new_application).not_to be_valid
46
- end
47
-
48
- it "checks uniqueness of uid" do
49
- app1 = FactoryBot.create(:application)
50
- app2 = FactoryBot.create(:application)
51
- app2.uid = app1.uid
52
- expect(app2).not_to be_valid
53
- end
54
-
55
- it "expects database to throw an error when uids are the same" do
56
- app1 = FactoryBot.create(:application)
57
- app2 = FactoryBot.create(:application)
58
- app2.uid = app1.uid
59
- expect { app2.save!(validate: false) }.to raise_error(uniqueness_error)
60
- end
16
+ context "application_owner is enabled" do
17
+ before do
18
+ Doorkeeper.configure do
19
+ orm DOORKEEPER_ORM
20
+ enable_application_owner
21
+ end
22
+ end
61
23
 
62
- it "generate secret on create" do
63
- expect(new_application.secret).to be_nil
64
- new_application.save
65
- expect(new_application.secret).not_to be_nil
66
- end
24
+ context "application owner is not required" do
25
+ before(:each) do
26
+ unset_require_owner
27
+ end
67
28
 
68
- it "generate secret on create if is blank string" do
69
- new_application.secret = ""
70
- new_application.save
71
- expect(new_application.secret).not_to be_blank
72
- end
29
+ it "is valid given valid attributes" do
30
+ expect(new_application).to be_valid
31
+ end
32
+ end
73
33
 
74
- it "generate secret on create unless one is set" do
75
- new_application.secret = secret
76
- new_application.save
77
- expect(new_application.secret).to eq(secret)
78
- end
34
+ context "application owner is required" do
35
+ before(:each) do
36
+ require_owner
37
+ @owner = FactoryBot.build_stubbed(:doorkeeper_testing_user)
38
+ end
79
39
 
80
- it "is invalid without secret" do
81
- new_application.save
82
- new_application.secret = nil
83
- expect(new_application).not_to be_valid
84
- end
40
+ it "is invalid without an owner" do
41
+ expect(new_application).not_to be_valid
42
+ end
85
43
 
86
- context "application_owner is enabled" do
87
- before do
88
- Doorkeeper.configure do
89
- orm DOORKEEPER_ORM
90
- enable_application_owner
44
+ it "is valid with an owner" do
45
+ new_application.owner = @owner
46
+ expect(new_application).to be_valid
47
+ end
91
48
  end
92
49
  end
93
50
 
94
- context "application owner is not required" do
95
- before(:each) do
96
- unset_require_owner
97
- end
98
-
99
- it "is valid given valid attributes" do
100
- expect(new_application).to be_valid
101
- end
51
+ it "is invalid without a name" do
52
+ new_application.name = nil
53
+ expect(new_application).not_to be_valid
102
54
  end
103
55
 
104
- context "application owner is required" do
105
- before do
106
- require_owner
107
- @owner = FactoryBot.build_stubbed(:doorkeeper_testing_user)
108
- end
56
+ it "is invalid without determining confidentiality" do
57
+ new_application.confidential = nil
58
+ expect(new_application).not_to be_valid
59
+ end
109
60
 
110
- it "is invalid without an owner" do
111
- expect(new_application).not_to be_valid
112
- end
61
+ it "generates uid on create" do
62
+ expect(new_application.uid).to be_nil
63
+ new_application.save
64
+ expect(new_application.uid).not_to be_nil
65
+ end
113
66
 
114
- it "is valid with an owner" do
115
- new_application.owner = @owner
116
- expect(new_application).to be_valid
117
- end
67
+ it "generates uid on create if an empty string" do
68
+ new_application.uid = ""
69
+ new_application.save
70
+ expect(new_application.uid).not_to be_blank
118
71
  end
119
- end
120
72
 
121
- context "redirect URI" do
122
- context "when grant flows allow blank redirect URI" do
123
- before do
124
- Doorkeeper.configure do
125
- grant_flows %w[password client_credentials]
126
- end
127
- end
73
+ it "generates uid on create unless one is set" do
74
+ new_application.uid = uid
75
+ new_application.save
76
+ expect(new_application.uid).to eq(uid)
77
+ end
128
78
 
129
- it "is valid without redirect_uri" do
130
- new_application.save
131
- new_application.redirect_uri = nil
132
- expect(new_application).to be_valid
133
- end
79
+ it "is invalid without uid" do
80
+ new_application.save
81
+ new_application.uid = nil
82
+ expect(new_application).not_to be_valid
134
83
  end
135
84
 
136
- context "when grant flows require redirect URI" do
137
- before do
138
- Doorkeeper.configure do
139
- grant_flows %w[password client_credentials authorization_code]
85
+ context "redirect URI" do
86
+ context "when grant flows allow blank redirect URI" do
87
+ before do
88
+ Doorkeeper.configure do
89
+ grant_flows %w[password client_credentials]
90
+ end
140
91
  end
141
- end
142
92
 
143
- it "is invalid without redirect_uri" do
144
- new_application.save
145
- new_application.redirect_uri = nil
146
- expect(new_application).not_to be_valid
93
+ it "is valid without redirect_uri" do
94
+ new_application.save
95
+ new_application.redirect_uri = nil
96
+ expect(new_application).to be_valid
97
+ end
147
98
  end
148
- end
149
99
 
150
- context "when blank URI option disabled" do
151
- before do
152
- Doorkeeper.configure do
153
- grant_flows %w[password client_credentials]
154
- allow_blank_redirect_uri false
100
+ context "when grant flows require redirect URI" do
101
+ before do
102
+ Doorkeeper.configure do
103
+ grant_flows %w[password client_credentials authorization_code]
104
+ end
155
105
  end
156
- end
157
106
 
158
- it "is invalid without redirect_uri" do
159
- new_application.save
160
- new_application.redirect_uri = nil
161
- expect(new_application).not_to be_valid
107
+ it "is invalid without redirect_uri" do
108
+ new_application.save
109
+ new_application.redirect_uri = nil
110
+ expect(new_application).not_to be_valid
111
+ end
162
112
  end
163
- end
164
- end
165
-
166
- context "with hashing enabled" do
167
- include_context "with application hashing enabled"
168
- let(:app) { FactoryBot.create :application }
169
- let(:default_strategy) { Doorkeeper::SecretStoring::Sha256Hash }
170
-
171
- it "uses SHA256 to avoid additional dependencies" do
172
- # Ensure token was generated
173
- app.validate
174
- expect(app.secret).to eq(default_strategy.transform_secret(app.plaintext_secret))
175
- end
176
113
 
177
- context "when bcrypt strategy is configured" do
178
- # In this text context, we have bcrypt loaded so `bcrypt_present?`
179
- # will always be true
180
- before do
181
- Doorkeeper.configure do
182
- hash_application_secrets using: "Doorkeeper::SecretStoring::BCrypt"
114
+ context "when blank URI option disabled" do
115
+ before do
116
+ Doorkeeper.configure do
117
+ grant_flows %w[password client_credentials]
118
+ allow_blank_redirect_uri false
119
+ end
183
120
  end
184
- end
185
121
 
186
- it "holds a volatile plaintext and BCrypt secret" do
187
- expect(app.secret_strategy).to eq Doorkeeper::SecretStoring::BCrypt
188
- expect(app.plaintext_secret).to be_a(String)
189
- expect(app.secret).not_to eq(app.plaintext_secret)
190
- expect { ::BCrypt::Password.create(app.secret) }.not_to raise_error
122
+ it "is invalid without redirect_uri" do
123
+ new_application.save
124
+ new_application.redirect_uri = nil
125
+ expect(new_application).not_to be_valid
126
+ end
191
127
  end
192
128
  end
193
129
 
194
- it "does not fallback to plain lookup by default" do
195
- lookup = described_class.by_uid_and_secret(app.uid, app.secret)
196
- expect(lookup).to eq(nil)
197
-
198
- lookup = described_class.by_uid_and_secret(app.uid, app.plaintext_secret)
199
- expect(lookup).to eq(app)
130
+ it "checks uniqueness of uid" do
131
+ app1 = FactoryBot.create(:application)
132
+ app2 = FactoryBot.create(:application)
133
+ app2.uid = app1.uid
134
+ expect(app2).not_to be_valid
200
135
  end
201
136
 
202
- context "with fallback enabled" do
203
- include_context "with token hashing and fallback lookup enabled"
204
-
205
- it "provides plain and hashed lookup" do
206
- lookup = described_class.by_uid_and_secret(app.uid, app.secret)
207
- expect(lookup).to eq(app)
208
-
209
- lookup = described_class.by_uid_and_secret(app.uid, app.plaintext_secret)
210
- expect(lookup).to eq(app)
211
- end
137
+ it "expects database to throw an error when uids are the same" do
138
+ app1 = FactoryBot.create(:application)
139
+ app2 = FactoryBot.create(:application)
140
+ app2.uid = app1.uid
141
+ expect { app2.save!(validate: false) }.to raise_error(uniqueness_error)
212
142
  end
213
143
 
214
- it "does not provide access to secret after loading" do
215
- lookup = described_class.by_uid_and_secret(app.uid, app.plaintext_secret)
216
- expect(lookup.plaintext_secret).to be_nil
144
+ it "generate secret on create" do
145
+ expect(new_application.secret).to be_nil
146
+ new_application.save
147
+ expect(new_application.secret).not_to be_nil
217
148
  end
218
- end
219
149
 
220
- describe "destroy related models on cascade" do
221
- before(:each) do
150
+ it "generate secret on create if is blank string" do
151
+ new_application.secret = ""
222
152
  new_application.save
153
+ expect(new_application.secret).not_to be_blank
223
154
  end
224
155
 
225
- let(:resource_owner) { FactoryBot.create(:doorkeeper_testing_user) }
226
-
227
- it "should destroy its access grants" do
228
- FactoryBot.create(
229
- :access_grant,
230
- application: new_application,
231
- resource_owner_id: resource_owner.id,
232
- )
233
-
234
- expect { new_application.destroy }.to change { Doorkeeper::AccessGrant.count }.by(-1)
156
+ it "generate secret on create unless one is set" do
157
+ new_application.secret = secret
158
+ new_application.save
159
+ expect(new_application.secret).to eq(secret)
235
160
  end
236
161
 
237
- it "should destroy its access tokens" do
238
- FactoryBot.create(:access_token, application: new_application)
239
- FactoryBot.create(:access_token, application: new_application, revoked_at: Time.now.utc)
240
- expect do
241
- new_application.destroy
242
- end.to change { Doorkeeper::AccessToken.count }.by(-2)
162
+ it "is invalid without secret" do
163
+ new_application.save
164
+ new_application.secret = nil
165
+ expect(new_application).not_to be_valid
243
166
  end
244
- end
245
167
 
246
- describe "#ordered_by" do
247
- let(:applications) { FactoryBot.create_list(:application, 5) }
168
+ context "with hashing enabled" do
169
+ include_context "with application hashing enabled"
170
+ let(:app) { FactoryBot.create :application }
171
+ let(:default_strategy) { Doorkeeper::SecretStoring::Sha256Hash }
248
172
 
249
- context "when a direction is not specified" do
250
- it "calls order with a default order of asc" do
251
- names = applications.map(&:name).sort
252
- expect(described_class.ordered_by(:name).map(&:name)).to eq(names)
173
+ it "uses SHA256 to avoid additional dependencies" do
174
+ # Ensure token was generated
175
+ app.validate
176
+ expect(app.secret).to eq(default_strategy.transform_secret(app.plaintext_secret))
253
177
  end
254
- end
255
178
 
256
- context "when a direction is specified" do
257
- it "calls order with specified direction" do
258
- names = applications.map(&:name).sort.reverse
259
- expect(described_class.ordered_by(:name, :desc).map(&:name)).to eq(names)
260
- end
261
- end
262
- end
179
+ context "when bcrypt strategy is configured" do
180
+ # In this text context, we have bcrypt loaded so `bcrypt_present?`
181
+ # will always be true
182
+ before do
183
+ Doorkeeper.configure do
184
+ hash_application_secrets using: "Doorkeeper::SecretStoring::BCrypt"
185
+ end
186
+ end
263
187
 
264
- describe "#redirect_uri=" do
265
- context "when array of valid redirect_uris" do
266
- it "should join by newline" do
267
- new_application.redirect_uri = ["http://localhost/callback1", "http://localhost/callback2"]
268
- expect(new_application.redirect_uri).to eq("http://localhost/callback1\nhttp://localhost/callback2")
269
- end
270
- end
271
- context "when string of valid redirect_uris" do
272
- it "should store as-is" do
273
- new_application.redirect_uri = "http://localhost/callback1\nhttp://localhost/callback2"
274
- expect(new_application.redirect_uri).to eq("http://localhost/callback1\nhttp://localhost/callback2")
188
+ it "holds a volatile plaintext and BCrypt secret" do
189
+ expect(app.secret_strategy).to eq Doorkeeper::SecretStoring::BCrypt
190
+ expect(app.plaintext_secret).to be_a(String)
191
+ expect(app.secret).not_to eq(app.plaintext_secret)
192
+ expect { ::BCrypt::Password.create(app.secret) }.not_to raise_error
193
+ end
275
194
  end
276
- end
277
- end
278
195
 
279
- describe "#authorized_for" do
280
- let(:resource_owner) { FactoryBot.create(:doorkeeper_testing_user) }
281
- let(:other_resource_owner) { FactoryBot.create(:doorkeeper_testing_user) }
196
+ it "does not fallback to plain lookup by default" do
197
+ lookup = clazz.by_uid_and_secret(app.uid, app.secret)
198
+ expect(lookup).to eq(nil)
282
199
 
283
- it "is empty if the application is not authorized for anyone" do
284
- expect(described_class.authorized_for(resource_owner)).to be_empty
285
- end
200
+ lookup = clazz.by_uid_and_secret(app.uid, app.plaintext_secret)
201
+ expect(lookup).to eq(app)
202
+ end
286
203
 
287
- it "returns only application for a specific resource owner" do
288
- FactoryBot.create(
289
- :access_token,
290
- resource_owner_id: other_resource_owner.id,
291
- )
292
- token = FactoryBot.create(
293
- :access_token,
294
- resource_owner_id: resource_owner.id,
295
- )
296
- expect(described_class.authorized_for(resource_owner)).to eq([token.application])
297
- end
204
+ context "with fallback enabled" do
205
+ include_context "with token hashing and fallback lookup enabled"
298
206
 
299
- it "excludes revoked tokens" do
300
- FactoryBot.create(
301
- :access_token,
302
- resource_owner_id: resource_owner.id,
303
- revoked_at: 2.days.ago,
304
- )
305
- expect(described_class.authorized_for(resource_owner)).to be_empty
306
- end
207
+ it "provides plain and hashed lookup" do
208
+ lookup = clazz.by_uid_and_secret(app.uid, app.secret)
209
+ expect(lookup).to eq(app)
307
210
 
308
- it "returns all applications that have been authorized" do
309
- token1 = FactoryBot.create(
310
- :access_token,
311
- resource_owner_id: resource_owner.id,
312
- )
313
- token2 = FactoryBot.create(
314
- :access_token,
315
- resource_owner_id: resource_owner.id,
316
- )
317
- expect(described_class.authorized_for(resource_owner))
318
- .to eq([token1.application, token2.application])
319
- end
211
+ lookup = clazz.by_uid_and_secret(app.uid, app.plaintext_secret)
212
+ expect(lookup).to eq(app)
213
+ end
214
+ end
320
215
 
321
- it "returns only one application even if it has been authorized twice" do
322
- application = FactoryBot.create(:application)
323
- FactoryBot.create(
324
- :access_token,
325
- resource_owner_id: resource_owner.id,
326
- application: application,
327
- )
328
- FactoryBot.create(
329
- :access_token,
330
- resource_owner_id: resource_owner.id,
331
- application: application,
332
- )
333
- expect(described_class.authorized_for(resource_owner)).to eq([application])
216
+ it "does not provide access to secret after loading" do
217
+ lookup = clazz.by_uid_and_secret(app.uid, app.plaintext_secret)
218
+ expect(lookup.plaintext_secret).to be_nil
219
+ end
334
220
  end
335
- end
336
221
 
337
- describe "#revoke_tokens_and_grants_for" do
338
- it "revokes all access tokens and access grants" do
339
- application_id = 42
340
- resource_owner = double
341
- expect(Doorkeeper::AccessToken)
342
- .to receive(:revoke_all_for).with(application_id, resource_owner)
343
- expect(Doorkeeper::AccessGrant)
344
- .to receive(:revoke_all_for).with(application_id, resource_owner)
222
+ describe "destroy related models on cascade" do
223
+ before(:each) do
224
+ new_application.save
225
+ end
226
+
227
+ it "should destroy its access grants" do
228
+ FactoryBot.create(:access_grant, application: new_application)
229
+ expect { new_application.destroy }.to change { Doorkeeper::AccessGrant.count }.by(-1)
230
+ end
345
231
 
346
- described_class.revoke_tokens_and_grants_for(application_id, resource_owner)
232
+ it "should destroy its access tokens" do
233
+ FactoryBot.create(:access_token, application: new_application)
234
+ FactoryBot.create(:access_token, application: new_application, revoked_at: Time.now.utc)
235
+ expect do
236
+ new_application.destroy
237
+ end.to change { Doorkeeper::AccessToken.count }.by(-2)
238
+ end
347
239
  end
348
- end
349
240
 
350
- describe "#by_uid_and_secret" do
351
- context "when application is private/confidential" do
352
- it "finds the application via uid/secret" do
353
- app = FactoryBot.create :application
354
- authenticated = described_class.by_uid_and_secret(app.uid, app.secret)
355
- expect(authenticated).to eq(app)
241
+ describe :ordered_by do
242
+ let(:applications) { FactoryBot.create_list(:application, 5) }
243
+
244
+ context "when a direction is not specified" do
245
+ it "calls order with a default order of asc" do
246
+ names = applications.map(&:name).sort
247
+ expect(Application.ordered_by(:name).map(&:name)).to eq(names)
248
+ end
356
249
  end
357
- context "when secret is wrong" do
358
- it "should not find the application" do
359
- app = FactoryBot.create :application
360
- authenticated = described_class.by_uid_and_secret(app.uid, "bad")
361
- expect(authenticated).to eq(nil)
250
+
251
+ context "when a direction is specified" do
252
+ it "calls order with specified direction" do
253
+ names = applications.map(&:name).sort.reverse
254
+ expect(Application.ordered_by(:name, :desc).map(&:name)).to eq(names)
362
255
  end
363
256
  end
364
257
  end
365
258
 
366
- context "when application is public/non-confidential" do
367
- context "when secret is blank" do
368
- it "should find the application" do
369
- app = FactoryBot.create :application, confidential: false
370
- authenticated = described_class.by_uid_and_secret(app.uid, nil)
371
- expect(authenticated).to eq(app)
259
+ describe "#redirect_uri=" do
260
+ context "when array of valid redirect_uris" do
261
+ it "should join by newline" do
262
+ new_application.redirect_uri = ["http://localhost/callback1", "http://localhost/callback2"]
263
+ expect(new_application.redirect_uri).to eq("http://localhost/callback1\nhttp://localhost/callback2")
372
264
  end
373
265
  end
374
- context "when secret is wrong" do
375
- it "should not find the application" do
376
- app = FactoryBot.create :application, confidential: false
377
- authenticated = described_class.by_uid_and_secret(app.uid, "bad")
378
- expect(authenticated).to eq(nil)
266
+ context "when string of valid redirect_uris" do
267
+ it "should store as-is" do
268
+ new_application.redirect_uri = "http://localhost/callback1\nhttp://localhost/callback2"
269
+ expect(new_application.redirect_uri).to eq("http://localhost/callback1\nhttp://localhost/callback2")
379
270
  end
380
271
  end
381
272
  end
382
- end
383
-
384
- describe "#confidential?" do
385
- subject { FactoryBot.create(:application, confidential: confidential).confidential? }
386
-
387
- context "when application is private/confidential" do
388
- let(:confidential) { true }
389
- it { expect(subject).to eq(true) }
390
- end
391
-
392
- context "when application is public/non-confidential" do
393
- let(:confidential) { false }
394
- it { expect(subject).to eq(false) }
395
- end
396
- end
397
273
 
398
- describe "#as_json" do
399
- let(:app) { FactoryBot.create :application, secret: "123123123" }
274
+ describe :authorized_for do
275
+ let(:resource_owner) { double(:resource_owner, id: 10) }
400
276
 
401
- before do
402
- allow(Doorkeeper.configuration)
403
- .to receive(:application_secret_strategy).and_return(Doorkeeper::SecretStoring::Plain)
404
- end
277
+ it "is empty if the application is not authorized for anyone" do
278
+ expect(Application.authorized_for(resource_owner)).to be_empty
279
+ end
405
280
 
406
- # AR specific feature
407
- if DOORKEEPER_ORM == :active_record
408
- it "correctly works with #to_json" do
409
- ActiveRecord::Base.include_root_in_json = true
410
- expect(app.to_json(include_root_in_json: true)).to match(/application.+?:\{/)
411
- ActiveRecord::Base.include_root_in_json = false
281
+ it "returns only application for a specific resource owner" do
282
+ FactoryBot.create(:access_token, resource_owner_id: resource_owner.id + 1)
283
+ token = FactoryBot.create(:access_token, resource_owner_id: resource_owner.id)
284
+ expect(Application.authorized_for(resource_owner)).to eq([token.application])
412
285
  end
413
- end
414
286
 
415
- context "when called without authorized resource owner" do
416
- it "includes minimal set of attributes" do
417
- expect(app.as_json).to match(
418
- "id" => app.id,
419
- "name" => app.name,
420
- "created_at" => an_instance_of(String),
421
- )
287
+ it "excludes revoked tokens" do
288
+ FactoryBot.create(:access_token, resource_owner_id: resource_owner.id, revoked_at: 2.days.ago)
289
+ expect(Application.authorized_for(resource_owner)).to be_empty
422
290
  end
423
291
 
424
- it "includes application UID if it's public" do
425
- app = FactoryBot.create :application, secret: "123123123", confidential: false
292
+ it "returns all applications that have been authorized" do
293
+ token1 = FactoryBot.create(:access_token, resource_owner_id: resource_owner.id)
294
+ token2 = FactoryBot.create(:access_token, resource_owner_id: resource_owner.id)
295
+ expect(Application.authorized_for(resource_owner)).to eq([token1.application, token2.application])
296
+ end
426
297
 
427
- expect(app.as_json).to match(
428
- "id" => app.id,
429
- "name" => app.name,
430
- "created_at" => an_instance_of(String),
431
- "uid" => app.uid,
432
- )
298
+ it "returns only one application even if it has been authorized twice" do
299
+ application = FactoryBot.create(:application)
300
+ FactoryBot.create(:access_token, resource_owner_id: resource_owner.id, application: application)
301
+ FactoryBot.create(:access_token, resource_owner_id: resource_owner.id, application: application)
302
+ expect(Application.authorized_for(resource_owner)).to eq([application])
433
303
  end
304
+ end
434
305
 
435
- it "respects custom options" do
436
- expect(app.as_json(except: :id)).not_to include("id")
437
- expect(app.as_json(only: %i[name created_at secret]))
438
- .to match(
439
- "name" => app.name,
440
- "created_at" => an_instance_of(String),
441
- )
306
+ describe :revoke_tokens_and_grants_for do
307
+ it "revokes all access tokens and access grants" do
308
+ application_id = 42
309
+ resource_owner = double
310
+ expect(Doorkeeper::AccessToken)
311
+ .to receive(:revoke_all_for).with(application_id, resource_owner)
312
+ expect(Doorkeeper::AccessGrant)
313
+ .to receive(:revoke_all_for).with(application_id, resource_owner)
314
+
315
+ Application.revoke_tokens_and_grants_for(application_id, resource_owner)
442
316
  end
443
317
  end
444
318
 
445
- context "when called with authorized resource owner" do
446
- let(:owner) { FactoryBot.create(:doorkeeper_testing_user) }
447
- let(:other_owner) { FactoryBot.create(:doorkeeper_testing_user) }
448
- let(:app) { FactoryBot.create(:application, secret: "123123123", owner: owner) }
319
+ describe :by_uid_and_secret do
320
+ context "when application is private/confidential" do
321
+ it "finds the application via uid/secret" do
322
+ app = FactoryBot.create :application
323
+ authenticated = Application.by_uid_and_secret(app.uid, app.secret)
324
+ expect(authenticated).to eq(app)
325
+ end
326
+ context "when secret is wrong" do
327
+ it "should not find the application" do
328
+ app = FactoryBot.create :application
329
+ authenticated = Application.by_uid_and_secret(app.uid, "bad")
330
+ expect(authenticated).to eq(nil)
331
+ end
332
+ end
333
+ end
449
334
 
450
- before do
451
- Doorkeeper.configure do
452
- orm DOORKEEPER_ORM
453
- enable_application_owner confirmation: false
335
+ context "when application is public/non-confidential" do
336
+ context "when secret is blank" do
337
+ it "should find the application" do
338
+ app = FactoryBot.create :application, confidential: false
339
+ authenticated = Application.by_uid_and_secret(app.uid, nil)
340
+ expect(authenticated).to eq(app)
341
+ end
342
+ end
343
+ context "when secret is wrong" do
344
+ it "should not find the application" do
345
+ app = FactoryBot.create :application, confidential: false
346
+ authenticated = Application.by_uid_and_secret(app.uid, "bad")
347
+ expect(authenticated).to eq(nil)
348
+ end
454
349
  end
455
350
  end
351
+ end
352
+
353
+ describe :confidential? do
354
+ subject { FactoryBot.create(:application, confidential: confidential).confidential? }
456
355
 
457
- it "includes all the attributes" do
458
- expect(app.as_json(current_resource_owner: owner))
459
- .to include(
460
- "secret" => "123123123",
461
- "redirect_uri" => app.redirect_uri,
462
- "uid" => app.uid,
463
- )
356
+ context "when application is private/confidential" do
357
+ let(:confidential) { true }
358
+ it { expect(subject).to eq(true) }
464
359
  end
465
360
 
466
- it "doesn't include unsafe attributes if current owner isn't the same as owner" do
467
- expect(app.as_json(current_resource_owner: other_owner))
468
- .not_to include("redirect_uri")
361
+ context "when application is public/non-confidential" do
362
+ let(:confidential) { false }
363
+ it { expect(subject).to eq(false) }
469
364
  end
470
365
  end
471
366
  end