sorcery 0.13.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +5 -0
  4. data/.github/workflows/ruby.yml +49 -0
  5. data/.rubocop.yml +2 -2
  6. data/.rubocop_todo.yml +157 -1
  7. data/CHANGELOG.md +49 -0
  8. data/CODE_OF_CONDUCT.md +14 -0
  9. data/Gemfile +1 -1
  10. data/README.md +4 -4
  11. data/Rakefile +3 -1
  12. data/SECURITY.md +19 -0
  13. data/gemfiles/rails_52.gemfile +7 -0
  14. data/gemfiles/rails_60.gemfile +7 -0
  15. data/lib/generators/sorcery/helpers.rb +4 -0
  16. data/lib/generators/sorcery/templates/initializer.rb +111 -85
  17. data/lib/generators/sorcery/templates/migration/activity_logging.rb +5 -5
  18. data/lib/generators/sorcery/templates/migration/brute_force_protection.rb +4 -4
  19. data/lib/generators/sorcery/templates/migration/core.rb +4 -4
  20. data/lib/generators/sorcery/templates/migration/external.rb +3 -3
  21. data/lib/generators/sorcery/templates/migration/magic_login.rb +4 -4
  22. data/lib/generators/sorcery/templates/migration/remember_me.rb +3 -3
  23. data/lib/generators/sorcery/templates/migration/reset_password.rb +5 -5
  24. data/lib/generators/sorcery/templates/migration/user_activation.rb +4 -4
  25. data/lib/sorcery/adapters/active_record_adapter.rb +2 -2
  26. data/lib/sorcery/controller.rb +4 -1
  27. data/lib/sorcery/controller/config.rb +6 -6
  28. data/lib/sorcery/controller/submodules/activity_logging.rb +5 -3
  29. data/lib/sorcery/controller/submodules/external.rb +4 -1
  30. data/lib/sorcery/controller/submodules/http_basic_auth.rb +1 -0
  31. data/lib/sorcery/controller/submodules/remember_me.rb +2 -1
  32. data/lib/sorcery/controller/submodules/session_timeout.rb +2 -0
  33. data/lib/sorcery/crypto_providers/aes256.rb +1 -1
  34. data/lib/sorcery/crypto_providers/bcrypt.rb +6 -1
  35. data/lib/sorcery/engine.rb +7 -1
  36. data/lib/sorcery/model.rb +6 -5
  37. data/lib/sorcery/model/config.rb +5 -0
  38. data/lib/sorcery/model/submodules/magic_login.rb +7 -4
  39. data/lib/sorcery/model/submodules/reset_password.rb +6 -2
  40. data/lib/sorcery/providers/battlenet.rb +51 -0
  41. data/lib/sorcery/providers/discord.rb +52 -0
  42. data/lib/sorcery/providers/line.rb +63 -0
  43. data/lib/sorcery/providers/linkedin.rb +45 -36
  44. data/lib/sorcery/providers/vk.rb +1 -1
  45. data/lib/sorcery/version.rb +1 -1
  46. data/sorcery.gemspec +5 -6
  47. data/spec/controllers/controller_oauth2_spec.rb +41 -6
  48. data/spec/controllers/controller_oauth_spec.rb +6 -0
  49. data/spec/controllers/controller_remember_me_spec.rb +15 -12
  50. data/spec/controllers/controller_spec.rb +11 -1
  51. data/spec/providers/example_provider_spec.rb +17 -0
  52. data/spec/providers/example_spec.rb +17 -0
  53. data/spec/rails_app/app/assets/config/manifest.js +1 -0
  54. data/spec/rails_app/app/controllers/application_controller.rb +2 -0
  55. data/spec/rails_app/app/controllers/sorcery_controller.rb +69 -1
  56. data/spec/rails_app/config/routes.rb +10 -0
  57. data/spec/shared_examples/user_reset_password_shared_examples.rb +18 -2
  58. data/spec/shared_examples/user_shared_examples.rb +63 -0
  59. data/spec/sorcery_crypto_providers_spec.rb +60 -0
  60. data/spec/support/migration_helper.rb +12 -2
  61. data/spec/support/providers/example.rb +11 -0
  62. data/spec/support/providers/example_provider.rb +11 -0
  63. metadata +25 -15
  64. data/.travis.yml +0 -38
  65. data/gemfiles/active_record_rails_40.gemfile +0 -6
  66. data/gemfiles/active_record_rails_41.gemfile +0 -6
  67. data/gemfiles/active_record_rails_42.gemfile +0 -6
@@ -150,6 +150,16 @@ describe SorceryController, type: :controller do
150
150
  end
151
151
  end
152
152
 
153
+ it 'require_login before_action does not save the url for JSON requests' do
154
+ get :some_action, format: :json
155
+ expect(session[:return_to_url]).to be_nil
156
+ end
157
+
158
+ it 'require_login before_action does not save the url for XHR requests' do
159
+ get :some_action, xhr: true
160
+ expect(session[:return_to_url]).to be_nil
161
+ end
162
+
153
163
  it 'on successful login the user is redirected to the url he originally wanted' do
154
164
  session[:return_to_url] = 'http://test.host/some_action'
155
165
  post :test_return_to, params: { email: 'bla@bla.com', password: 'secret' }
@@ -161,7 +171,7 @@ describe SorceryController, type: :controller do
161
171
  # --- auto_login(user) ---
162
172
  specify { should respond_to(:auto_login) }
163
173
 
164
- it 'auto_login(user) los in a user instance' do
174
+ it 'auto_login(user) logs in a user instance' do
165
175
  session[:user_id] = nil
166
176
  subject.auto_login(user)
167
177
 
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'sorcery/providers/base'
5
+
6
+ describe Sorcery::Providers::ExampleProvider do
7
+ before(:all) do
8
+ sorcery_reload!([:external])
9
+ sorcery_controller_property_set(:external_providers, [:example_provider])
10
+ end
11
+
12
+ context 'fetching a multi-word custom provider' do
13
+ it 'returns the provider' do
14
+ expect(Sorcery::Controller::Config.example_provider).to be_a(Sorcery::Providers::ExampleProvider)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'sorcery/providers/base'
5
+
6
+ describe Sorcery::Providers::Example do
7
+ before(:all) do
8
+ sorcery_reload!([:external])
9
+ sorcery_controller_property_set(:external_providers, [:example])
10
+ end
11
+
12
+ context 'fetching a single-word custom provider' do
13
+ it 'returns the provider' do
14
+ expect(Sorcery::Controller::Config.example).to be_a(Sorcery::Providers::Example)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,2 @@
1
+ class ApplicationController < ActionController::Base
2
+ end
@@ -1,11 +1,12 @@
1
1
  require 'oauth'
2
2
 
3
- class SorceryController < ActionController::Base
3
+ class SorceryController < ApplicationController
4
4
  protect_from_forgery
5
5
 
6
6
  before_action :require_login_from_http_basic, only: [:test_http_basic_auth]
7
7
  before_action :require_login, only: %i[
8
8
  test_logout
9
+ test_logout_with_forget_me
9
10
  test_logout_with_force_forget_me
10
11
  test_should_be_logged_in
11
12
  some_action
@@ -50,6 +51,13 @@ class SorceryController < ActionController::Base
50
51
  head :ok
51
52
  end
52
53
 
54
+ def test_logout_with_forget_me
55
+ remember_me!
56
+ forget_me!
57
+ logout
58
+ head :ok
59
+ end
60
+
53
61
  def test_logout_with_force_forget_me
54
62
  remember_me!
55
63
  force_forget_me!
@@ -142,6 +150,10 @@ class SorceryController < ActionController::Base
142
150
  login_at(:slack)
143
151
  end
144
152
 
153
+ def login_at_test_line
154
+ login_at(:line)
155
+ end
156
+
145
157
  def login_at_test_with_state
146
158
  login_at(:facebook, state: 'bla')
147
159
  end
@@ -154,6 +166,14 @@ class SorceryController < ActionController::Base
154
166
  login_at(:auth0)
155
167
  end
156
168
 
169
+ def login_at_test_discord
170
+ login_at(:discord)
171
+ end
172
+
173
+ def login_at_test_battlenet
174
+ login_at(:battlenet)
175
+ end
176
+
157
177
  def test_login_from_twitter
158
178
  if (@user = login_from(:twitter))
159
179
  redirect_to 'bla', notice: 'Success!'
@@ -268,6 +288,30 @@ class SorceryController < ActionController::Base
268
288
  end
269
289
  end
270
290
 
291
+ def test_login_from_line
292
+ if @user = login_from(:line)
293
+ redirect_to 'bla', notice: 'Success!'
294
+ else
295
+ redirect_to 'blu', alert: 'Failed!'
296
+ end
297
+ end
298
+
299
+ def test_login_from_discord
300
+ if (@user = login_from(:discord))
301
+ redirect_to 'bla', notice: 'Success!'
302
+ else
303
+ redirect_to 'blu', alert: 'Failed!'
304
+ end
305
+ end
306
+
307
+ def test_login_from_battlenet
308
+ if (@user = login_from(:battlenet))
309
+ redirect_to 'bla', notice: 'Success!'
310
+ else
311
+ redirect_to 'blu', alert: 'Failed!'
312
+ end
313
+ end
314
+
271
315
  def test_return_to_with_external_twitter
272
316
  if (@user = login_from(:twitter))
273
317
  redirect_back_or_to 'bla', notice: 'Success!'
@@ -382,6 +426,30 @@ class SorceryController < ActionController::Base
382
426
  end
383
427
  end
384
428
 
429
+ def test_return_to_with_external_line
430
+ if @user = login_from(:line)
431
+ redirect_back_or_to 'bla', notice: 'Success!'
432
+ else
433
+ redirect_to 'blu', alert: 'Failed!'
434
+ end
435
+ end
436
+
437
+ def test_return_to_with_external_discord
438
+ if (@user = login_from(:discord))
439
+ redirect_back_or_to 'bla', notice: 'Success!'
440
+ else
441
+ redirect_to 'blu', alert: 'Failed!'
442
+ end
443
+ end
444
+
445
+ def test_return_to_with_external_battlenet
446
+ if (@user = login_from(:battlenet))
447
+ redirect_back_or_to 'bla', notice: 'Success!'
448
+ else
449
+ redirect_to 'blu', alert: 'Failed!'
450
+ end
451
+ end
452
+
385
453
  def test_create_from_provider
386
454
  provider = params[:provider]
387
455
  login_from(provider)
@@ -11,6 +11,7 @@ AppRoot::Application.routes.draw do
11
11
  get :test_login_from_cookie
12
12
  get :test_login_from
13
13
  get :test_logout_with_remember
14
+ get :test_logout_with_forget_me
14
15
  get :test_logout_with_force_forget_me
15
16
  get :test_invalidate_active_session
16
17
  get :test_should_be_logged_in
@@ -32,6 +33,9 @@ AppRoot::Application.routes.draw do
32
33
  get :test_login_from_slack
33
34
  get :test_login_from_instagram
34
35
  get :test_login_from_auth0
36
+ get :test_login_from_line
37
+ get :test_login_from_discord
38
+ get :test_login_from_battlenet
35
39
  get :login_at_test
36
40
  get :login_at_test_twitter
37
41
  get :login_at_test_facebook
@@ -47,6 +51,9 @@ AppRoot::Application.routes.draw do
47
51
  get :login_at_test_slack
48
52
  get :login_at_test_instagram
49
53
  get :login_at_test_auth0
54
+ get :login_at_test_line
55
+ get :login_at_test_discord
56
+ get :login_at_test_battlenet
50
57
  get :test_return_to_with_external
51
58
  get :test_return_to_with_external_twitter
52
59
  get :test_return_to_with_external_facebook
@@ -62,6 +69,9 @@ AppRoot::Application.routes.draw do
62
69
  get :test_return_to_with_external_slack
63
70
  get :test_return_to_with_external_instagram
64
71
  get :test_return_to_with_external_auth0
72
+ get :test_return_to_with_external_line
73
+ get :test_return_to_with_external_discord
74
+ get :test_return_to_with_external_battlenet
65
75
  get :test_http_basic_auth
66
76
  get :some_action_making_a_non_persisted_change_to_the_user
67
77
  post :test_login_with_remember
@@ -14,6 +14,8 @@ shared_examples_for 'rails_3_reset_password_model' do
14
14
  context 'API' do
15
15
  specify { expect(user).to respond_to :deliver_reset_password_instructions! }
16
16
 
17
+ specify { expect(user).to respond_to :change_password }
18
+
17
19
  specify { expect(user).to respond_to :change_password! }
18
20
 
19
21
  it 'responds to .load_from_reset_password_token' do
@@ -314,13 +316,27 @@ shared_examples_for 'rails_3_reset_password_model' do
314
316
  end
315
317
  end
316
318
 
317
- it 'when change_password! is called, deletes reset_password_token' do
319
+ it 'when change_password! is called, deletes reset_password_token and calls #save!' do
318
320
  user.deliver_reset_password_instructions!
319
321
 
320
322
  expect(user.reset_password_token).not_to be_nil
323
+ expect(user).to_not receive(:save)
324
+ expect(user).to receive(:save!)
321
325
 
322
326
  user.change_password!('blabulsdf')
323
- user.save!
327
+
328
+ expect(user.reset_password_token).to be_nil
329
+ end
330
+
331
+ it 'when change_password is called, deletes reset_password_token and calls #save' do
332
+ new_password = 'blabulsdf'
333
+
334
+ user.deliver_reset_password_instructions!
335
+ expect(user.reset_password_token).not_to be_nil
336
+ expect(user).to_not receive(:save!)
337
+ expect(user).to receive(:save)
338
+
339
+ user.change_password(new_password)
324
340
 
325
341
  expect(user.reset_password_token).to be_nil
326
342
  end
@@ -54,6 +54,13 @@ shared_examples_for 'rails_3_core_model' do
54
54
  expect(User.sorcery_config.custom_encryption_provider).to eq Array
55
55
  end
56
56
 
57
+ it "enables configuration option 'pepper'" do
58
+ pepper = '*$%&%*++'
59
+ sorcery_model_property_set(:pepper, pepper)
60
+
61
+ expect(User.sorcery_config.pepper).to eq pepper
62
+ end
63
+
57
64
  it "enables configuration option 'salt_join_token'" do
58
65
  salt_join_token = '--%%*&-'
59
66
  sorcery_model_property_set(:salt_join_token, salt_join_token)
@@ -459,6 +466,14 @@ shared_examples_for 'rails_3_core_model' do
459
466
  expect(User.encrypt(@text)).to eq Sorcery::CryptoProviders::SHA512.encrypt(@text)
460
467
  end
461
468
 
469
+ it 'if encryption algo is bcrypt it works' do
470
+ sorcery_model_property_set(:encryption_algorithm, :bcrypt)
471
+
472
+ # comparison is done using BCrypt::Password#==(raw_token), not by String#==
473
+ expect(User.encrypt(@text)).to be_an_instance_of BCrypt::Password
474
+ expect(User.encrypt(@text)).to eq @text
475
+ end
476
+
462
477
  it 'salt is random for each user and saved in db' do
463
478
  sorcery_model_property_set(:salt_attribute_name, :salt)
464
479
 
@@ -488,6 +503,54 @@ shared_examples_for 'rails_3_core_model' do
488
503
 
489
504
  expect(user.crypted_password).to eq Sorcery::CryptoProviders::SHA512.encrypt('secret', user.salt)
490
505
  end
506
+
507
+ it 'if pepper is set uses it to encrypt' do
508
+ sorcery_model_property_set(:salt_attribute_name, :salt)
509
+ sorcery_model_property_set(:pepper, '++@^$')
510
+ sorcery_model_property_set(:encryption_algorithm, :bcrypt)
511
+
512
+ # password comparison is done using BCrypt::Password#==(raw_token), not String#==
513
+ bcrypt_password = BCrypt::Password.new(user.crypted_password)
514
+ allow(::BCrypt::Password).to receive(:create) do |token, options = {}|
515
+ # need to use common BCrypt's salt when genarating BCrypt::Password objects
516
+ # so that any generated password hashes can be compared each other
517
+ ::BCrypt::Engine.hash_secret(token, bcrypt_password.salt)
518
+ end
519
+
520
+ expect(user.crypted_password).not_to eq Sorcery::CryptoProviders::BCrypt.encrypt('secret')
521
+
522
+ Sorcery::CryptoProviders::BCrypt.pepper = ''
523
+
524
+ expect(user.crypted_password).not_to eq Sorcery::CryptoProviders::BCrypt.encrypt('secret', user.salt)
525
+
526
+ Sorcery::CryptoProviders::BCrypt.pepper = User.sorcery_config.pepper
527
+
528
+ expect(user.crypted_password).to eq Sorcery::CryptoProviders::BCrypt.encrypt('secret', user.salt)
529
+ end
530
+
531
+ it 'if pepper is empty string (default) does not use pepper to encrypt' do
532
+ sorcery_model_property_set(:salt_attribute_name, :salt)
533
+ sorcery_model_property_set(:pepper, '')
534
+ sorcery_model_property_set(:encryption_algorithm, :bcrypt)
535
+
536
+ # password comparison is done using BCrypt::Password#==(raw_token), not String#==
537
+ bcrypt_password = BCrypt::Password.new(user.crypted_password)
538
+ allow(::BCrypt::Password).to receive(:create) do |token, options = {}|
539
+ # need to use common BCrypt's salt when genarating BCrypt::Password objects
540
+ # so that any generated password hashes can be compared each other
541
+ ::BCrypt::Engine.hash_secret(token, bcrypt_password.salt)
542
+ end
543
+
544
+ expect(user.crypted_password).not_to eq Sorcery::CryptoProviders::BCrypt.encrypt('secret')
545
+
546
+ Sorcery::CryptoProviders::BCrypt.pepper = 'some_pepper'
547
+
548
+ expect(user.crypted_password).not_to eq Sorcery::CryptoProviders::BCrypt.encrypt('secret', user.salt)
549
+
550
+ Sorcery::CryptoProviders::BCrypt.pepper = User.sorcery_config.pepper
551
+
552
+ expect(user.crypted_password).to eq Sorcery::CryptoProviders::BCrypt.encrypt('secret', user.salt)
553
+ end
491
554
  end
492
555
 
493
556
  describe 'ORM adapter' do
@@ -148,6 +148,7 @@ describe 'Crypto Providers wrappers' do
148
148
  before(:all) do
149
149
  Sorcery::CryptoProviders::BCrypt.cost = 1
150
150
  @digest = BCrypt::Password.create('Noam Ben-Ari', cost: Sorcery::CryptoProviders::BCrypt.cost)
151
+ @tokens = %w[password gq18WBnJYNh2arkC1kgH]
151
152
  end
152
153
 
153
154
  after(:each) do
@@ -181,5 +182,64 @@ describe 'Crypto Providers wrappers' do
181
182
  # stubbed in Sorcery::TestHelpers::Internal
182
183
  expect(Sorcery::CryptoProviders::BCrypt.cost).to eq 1
183
184
  end
185
+
186
+ it 'matches token encrypted with salt from upstream' do
187
+ # note: actual comparison is done by BCrypt::Password#==(raw_token)
188
+ expect(Sorcery::CryptoProviders::BCrypt.encrypt(@tokens)).to eq @tokens.flatten.join
189
+ end
190
+
191
+ it 'respond_to?(:pepper) returns true' do
192
+ expect(Sorcery::CryptoProviders::BCrypt.respond_to?(:pepper)).to be true
193
+ end
194
+
195
+ context 'when pepper is provided' do
196
+ before(:each) do
197
+ Sorcery::CryptoProviders::BCrypt.pepper = 'pepper'
198
+ @digest = Sorcery::CryptoProviders::BCrypt.encrypt(@tokens) # a BCrypt::Password object
199
+ end
200
+
201
+ it 'matches token encrypted with salt and pepper from upstream' do
202
+ # note: actual comparison is done by BCrypt::Password#==(raw_token)
203
+ expect(@digest).to eq @tokens.flatten.join.concat('pepper')
204
+ end
205
+
206
+ it 'matches? returns true when matches' do
207
+ expect(Sorcery::CryptoProviders::BCrypt.matches?(@digest, *@tokens)).to be true
208
+ end
209
+
210
+ it 'matches? returns false when pepper is replaced with empty string' do
211
+ Sorcery::CryptoProviders::BCrypt.pepper = ''
212
+ expect(Sorcery::CryptoProviders::BCrypt.matches?(@digest, *@tokens)).to be false
213
+ end
214
+
215
+ it 'matches? returns false when no match' do
216
+ expect(Sorcery::CryptoProviders::BCrypt.matches?(@digest, 'a_random_incorrect_password')).to be false
217
+ end
218
+ end
219
+
220
+ context "when pepper is an empty string (default)" do
221
+ before(:each) do
222
+ Sorcery::CryptoProviders::BCrypt.pepper = ''
223
+ @digest = Sorcery::CryptoProviders::BCrypt.encrypt(@tokens) # a BCrypt::Password object
224
+ end
225
+
226
+ # make sure the default pepper '' does nothing
227
+ it 'matches token encrypted with salt only (without pepper)' do
228
+ expect(@digest).to eq @tokens.flatten.join # keep consistency with the older versions of #join_token
229
+ end
230
+
231
+ it 'matches? returns true when matches' do
232
+ expect(Sorcery::CryptoProviders::BCrypt.matches?(@digest, *@tokens)).to be true
233
+ end
234
+
235
+ it 'matches? returns false when pepper has changed' do
236
+ Sorcery::CryptoProviders::BCrypt.pepper = 'a new pepper'
237
+ expect(Sorcery::CryptoProviders::BCrypt.matches?(@digest, *@tokens)).to be false
238
+ end
239
+
240
+ it 'matches? returns false when no match' do
241
+ expect(Sorcery::CryptoProviders::BCrypt.matches?(@digest, 'a_random_incorrect_password')).to be false
242
+ end
243
+ end
184
244
  end
185
245
  end
@@ -1,7 +1,9 @@
1
1
  class MigrationHelper
2
2
  class << self
3
3
  def migrate(path)
4
- if ActiveRecord.version >= Gem::Version.new('5.2.0')
4
+ if ActiveRecord.version >= Gem::Version.new('6.0.0')
5
+ ActiveRecord::MigrationContext.new(path, schema_migration).migrate
6
+ elsif ActiveRecord.version >= Gem::Version.new('5.2.0')
5
7
  ActiveRecord::MigrationContext.new(path).migrate
6
8
  else
7
9
  ActiveRecord::Migrator.migrate(path)
@@ -9,11 +11,19 @@ class MigrationHelper
9
11
  end
10
12
 
11
13
  def rollback(path)
12
- if ActiveRecord.version >= Gem::Version.new('5.2.0')
14
+ if ActiveRecord.version >= Gem::Version.new('6.0.0')
15
+ ActiveRecord::MigrationContext.new(path, schema_migration).rollback
16
+ elsif ActiveRecord.version >= Gem::Version.new('5.2.0')
13
17
  ActiveRecord::MigrationContext.new(path).rollback
14
18
  else
15
19
  ActiveRecord::Migrator.rollback(path)
16
20
  end
17
21
  end
22
+
23
+ private
24
+
25
+ def schema_migration
26
+ ActiveRecord::Base.connection.schema_migration
27
+ end
18
28
  end
19
29
  end