sorcery 0.13.0 → 0.16.1

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